Remove unneeded semicolons
[lhc/web/wiklou.git] / resources / mediawiki / mediawiki.js
index 5caafc3..1153045 100644 (file)
@@ -1,44 +1,57 @@
 /*
- * JavaScript backwards-compatibility and support
+ * JavaScript backwards-compatibility alternatives and other convenience functions
  */
 
-// Implementation of string trimming functionality introduced natively in JavaScript 1.8.1
-if ( typeof String.prototype.trim === 'undefined' ) {
-       // Add removing trailing and leading whitespace functionality cross-browser
-       // See also: http://blog.stevenlevithan.com/archives/faster-trim-javascript
-       String.prototype.trim = function() {
-               return this.replace( /^\s+|\s+$/g, '' );
-       };
-}
-if ( typeof String.prototype.trimLeft === 'undefined' ) {
-       String.prototype.trimLeft = function() {
-               return this.replace( /^\s\s*/, "" );
-       };
-}
-if ( typeof String.prototype.trimRight === 'undefined' ) {
-       String.prototype.trimRight = function() {
-               return this.replace(/\s\s*$/, "");
-       };
-}
-
-/*
- * Prototype enhancements
- */
-
-// Capitalize the first character of the given string
-if ( typeof String.prototype.ucFirst === 'undefined' ) {
-       String.prototype.ucFirst = function() {
-               return this.substr(0, 1).toUpperCase() + this.substr(1, this.length);
-       };
-}
-
-// Escape all RegExp special characters such that the result can be safely used
-// in a RegExp as a literal.
-if ( typeof String.prototype.escapeRE === 'undefined' ) {
-       String.prototype.escapeRE = function() {
-               return this.replace (/([\\{}()|.?*+^$\[\]])/g, "\\$1");
-       };
-}
+jQuery.extend({
+       trimLeft : function( str ) {
+               return str == null ? '' : str.toString().replace( /^\s+/, '' );
+       },
+       trimRight : function( str ) {
+               return str == null ?
+                               '' : str.toString().replace( /\s+$/, '' );
+       },
+       ucFirst : function( str ) {
+               return str.substr( 0, 1 ).toUpperCase() + str.substr( 1, str.length );
+       },
+       escapeRE : function( str ) {
+               return str.replace ( /([\\{}()|.?*+^$\[\]])/g, "\\$1" );
+       },
+       isEmpty : function( v ) {
+               var key;
+               if ( v === "" || v === 0 || v === "0" || v === null
+                       || v === false || typeof v === 'undefined' )
+               {
+                       return true;
+               }
+               // the for-loop could potentially contain prototypes
+               // to avoid that we check it's length first
+               if ( v.length === 0 ) {
+                       return true;
+               }
+               if ( typeof v === 'object' ) {
+                       for ( key in v ) {
+                               return false;
+                       }
+                       return true;
+               }
+               return false;
+       },
+       compareArray : function( arrThis, arrAgainst ) {
+               if ( arrThis.length != arrAgainst.length ) {
+                       return false;
+               }
+               for ( var i = 0; i < arrThis.length; i++ ) {
+                       if ( arrThis[i] instanceof Array ) {
+                               if ( !$.compareArray( arrThis[i], arrAgainst[i] ) ) {
+                                       return false;
+                               }
+                       } else if ( arrThis[i] !== arrAgainst[i] ) {
+                               return false;
+                       }
+               }
+               return true;
+       }
+});
 
 /*
  * Core MediaWiki JavaScript Library
@@ -46,37 +59,37 @@ if ( typeof String.prototype.escapeRE === 'undefined' ) {
 
 // Attach to window
 window.mediaWiki = new ( function( $ ) {
-       
+
        /* Constants */
-       
+
        // This will not change until we are 100% ready to turn off legacy globals
        var LEGACY_GLOBALS = true;
-       
+
        /* Private Members */
-       
+
        // List of messages that have been requested to be loaded
        var messageQueue = {};
-       
+
        /* Prototypes */
-       
+
        /**
-        * An object which allows single and multiple get/set/exists functionality 
+        * An object which allows single and multiple get/set/exists functionality
         * on a list of key / value pairs.
-        * 
-        * @param {boolean} global Whether to get/set/exists values on the window 
+        *
+        * @param {boolean} global Whether to get/set/exists values on the window
         *   object or a private object
         */
        function Map( global ) {
                this.values = ( global === true ) ? window : {};
-       };
-       
+       }
+
        /**
         * Gets the value of a key, or a list of key/value pairs for an array of keys.
-        * 
+        *
         * If called with no arguments, all values will be returned.
-        * 
-        * @param {mixed} selection Key or array of keys to get values for
-        * @param {mixed} fallback Value to use in case key(s) do not exist (optional)
+        *
+        * @param selection mixed Key or array of keys to get values for
+        * @param fallback mixed Value to use in case key(s) do not exist (optional)
         */
        Map.prototype.get = function( selection, fallback ) {
                if ( typeof selection === 'object' ) {
@@ -97,12 +110,12 @@ window.mediaWiki = new ( function( $ ) {
                }
                return this.values;
        };
-       
+
        /**
         * Sets one or multiple key/value pairs.
-        * 
-        * @param {mixed} selection Key or object of key/value pairs to set
-        * @param {mixed} value Value to set (optional, only in use when key is a string)
+        *
+        * @param selection mixed Key or object of key/value pairs to set
+        * @param value mixed Value to set (optional, only in use when key is a string)
         */
        Map.prototype.set = function( selection, value ) {
                if ( typeof selection === 'object' ) {
@@ -113,12 +126,12 @@ window.mediaWiki = new ( function( $ ) {
                        this.values[selection] = value;
                }
        };
-       
+
        /**
         * Checks if one or multiple keys exist.
-        * 
-        * @param {mixed} key Key or array of keys to check
-        * @return {boolean} Existence of key(s)
+        *
+        * @param selection mixed Key or array of keys to check
+        * @return boolean Existence of key(s)
         */
        Map.prototype.exists = function( selection ) {
                if ( typeof keys === 'object' ) {
@@ -132,21 +145,21 @@ window.mediaWiki = new ( function( $ ) {
                        return selection in this.values;
                }
        };
-       
+
        /**
-        * Message object, similar to Message in PHP 
+        * Message object, similar to Message in PHP
         */
        function Message( map, key, parameters ) {
                this.format = 'parse';
                this.map = map;
                this.key = key;
                this.parameters = typeof parameters === 'undefined' ? [] : $.makeArray( parameters );
-       };
-       
+       }
+
        /**
         * Appends parameters for replacement
-        * 
-        * @param {mixed} args First in a list of variadic arguments to append as message parameters
+        *
+        * @param parameters mixed First in a list of variadic arguments to append as message parameters
         */
        Message.prototype.params = function( parameters ) {
                for ( var i = 0; i < parameters.length; i++ ) {
@@ -154,10 +167,10 @@ window.mediaWiki = new ( function( $ ) {
                }
                return this;
        };
-       
+
        /**
         * Converts message object to it's string form based on the state of format
-        * 
+        *
         * @return {string} String form of message
         */
        Message.prototype.toString = function() {
@@ -178,47 +191,102 @@ window.mediaWiki = new ( function( $ ) {
                */
                return text;
        };
-       
+
        /**
         * Changes format to parse and converts message to string
-        * 
+        *
         * @return {string} String form of parsed message
         */
        Message.prototype.parse = function() {
                this.format = 'parse';
                return this.toString();
        };
-       
+
        /**
         * Changes format to plain and converts message to string
-        * 
+        *
         * @return {string} String form of plain message
         */
        Message.prototype.plain = function() {
                this.format = 'plain';
                return this.toString();
        };
-       
+
        /**
         * Checks if message exists
-        * 
+        *
         * @return {string} String form of parsed message
         */
        Message.prototype.exists = function() {
                return this.map.exists( this.key );
        };
-       
+
        /**
         * User object
         */
        function User() {
+
+               /* Private Members */
+
+               var that = this;
+
+               /* Public Members */
+
                this.options = new Map();
+
+               /* Public Methods */
+
+               /*
+                * Generates a random user session ID (32 alpha-numeric characters).
+                * 
+                * This information would potentially be stored in a cookie to identify a user during a
+                * session. It's uniqueness should not be depended on.
+                * 
+                * @return string random set of 32 alpha-numeric characters
+                */
+               function generateSessionId() {
+                       var id = '';
+                       var seed = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz';
+                       for ( var i = 0, r; i < 32; i++ ) {
+                               r = Math.floor( Math.random() * seed.length );
+                               id += seed.substring( r, r + 1 );
+                       }
+                       return id;
+               }
+
+               /*
+                * Gets the current user's name.
+                * 
+                * @return mixed user name string or null if users is anonymous
+                */
+               this.name = function() {
+                       return mediaWiki.config.get( 'wgUserName' );
+               };
+
+               /*
+                * Gets the current user's name or a random session ID automatically generated and kept in
+                * a cookie.
+                * 
+                * @return string user name or random session ID
+                */
+               this.sessionId = function () {
+                       var name = that.name();
+                       if ( name ) {
+                               return name;
+                       }
+                       var sessionId = $.cookie( 'mediaWiki.user.sessionId' );
+                       if ( typeof sessionId == 'undefined' || sessionId == null ) {
+                               sessionId = generateSessionId();
+                               $.cookie( 'mediaWiki.user.sessionId', sessionId, { 'expires': 30, 'path': '/' } );
+                       }
+                       return sessionId;
+               };
        }
-       
+
        /* Public Members */
 
        /*
-        * Dummy function which in debug mode can be replaced with a function that 
+        * Dummy function which in debug mode can be replaced with a function that
         * does something clever
         */
        this.log = function() { };
@@ -234,19 +302,19 @@ window.mediaWiki = new ( function( $ ) {
         * Information about the current user
         */
        this.user = new User();
-       
+
        /*
         * Localization system
         */
        this.messages = new Map();
-       
+
        /* Public Methods */
-       
+
        /**
         * Gets a message object, similar to wfMessage()
-        * 
-        * @param {string} key Key of message to get
-        * @param {mixed} params First argument in a list of variadic arguments, each a parameter for $
+        *
+        * @param key string Key of message to get
+        * @param parameters mixed First argument in a list of variadic arguments, each a parameter for $
         * replacement
         */
        this.message = function( key, parameters ) {
@@ -259,32 +327,32 @@ window.mediaWiki = new ( function( $ ) {
                }
                return new Message( mediaWiki.messages, key, parameters );
        };
-       
+
        /**
         * Gets a message string, similar to wfMsg()
-        * 
-        * @param {string} key Key of message to get
-        * @param {mixed} params First argument in a list of variadic arguments, each a parameter for $
+        *
+        * @param key string Key of message to get
+        * @param parameters mixed First argument in a list of variadic arguments, each a parameter for $
         * replacement
         */
        this.msg = function( key, parameters ) {
                return mediaWiki.message.apply( mediaWiki.message, arguments ).toString();
        };
-       
+
        /**
         * Client-side module loader which integrates with the MediaWiki ResourceLoader
         */
        this.loader = new ( function() {
 
                /* Private Members */
-               
+
                /**
                 * Mapping of registered modules
                 *
-                * The jquery module is pre-registered, because it must have already 
-                * been provided for this object to have been built, and in debug mode 
-                * jquery would have been provided through a unique loader request, 
-                * making it impossible to hold back registration of jquery until after 
+                * The jquery module is pre-registered, because it must have already
+                * been provided for this object to have been built, and in debug mode
+                * jquery would have been provided through a unique loader request,
+                * making it impossible to hold back registration of jquery until after
                 * mediawiki.
                 *
                 * Format:
@@ -318,7 +386,7 @@ window.mediaWiki = new ( function( $ ) {
                                return false;
                        }
                        for ( var i = 0; i < b.length; i++ ) {
-                               if ( $.isArray( a[i] ) ) { 
+                               if ( $.isArray( a[i] ) ) {
                                        if ( !compare( a[i], b[i] ) ) {
                                                return false;
                                        }
@@ -328,8 +396,8 @@ window.mediaWiki = new ( function( $ ) {
                                }
                        }
                        return true;
-               };
-               
+               }
+
                /**
                 * Generates an ISO8601 "basic" string from a UNIX timestamp
                 */
@@ -337,7 +405,7 @@ window.mediaWiki = new ( function( $ ) {
                        function pad( a, b, c ) {
                                return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
                        }
-                       var d = new Date()
+                       var d = new Date();
                        d.setTime( timestamp * 1000 );
                        return [
                                pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
@@ -365,7 +433,7 @@ window.mediaWiki = new ( function( $ ) {
                                if ( $.inArray( registry[module].dependencies[n], resolved ) === -1 ) {
                                        if ( $.inArray( registry[module].dependencies[n], unresolved ) !== -1 ) {
                                                throw new Error(
-                                                       'Circular reference detected: ' + module + 
+                                                       'Circular reference detected: ' + module +
                                                        ' -> ' + registry[module].dependencies[n]
                                                );
                                        }
@@ -379,7 +447,7 @@ window.mediaWiki = new ( function( $ ) {
                /**
                 * Gets a list of module names that a module depends on in their proper dependency order
                 *
-                * @param mixed string module name or array of string module names
+                * @param module string module name or array of string module names
                 * @return list of dependencies
                 * @throws Error if circular reference is detected
                 */
@@ -404,15 +472,15 @@ window.mediaWiki = new ( function( $ ) {
                                return resolved;
                        }
                        throw new Error( 'Invalid module argument: ' + module );
-               };
+               }
 
                /**
-                * Narrows a list of module names down to those matching a specific 
-                * state. Possible states are 'undefined', 'registered', 'loading', 
+                * Narrows a list of module names down to those matching a specific
+                * state. Possible states are 'undefined', 'registered', 'loading',
                 * 'loaded', or 'ready'
                 *
-                * @param mixed string or array of strings of module states to filter by
-                * @param array list of module names to filter (optional, all modules 
+                * @param states string or array of strings of module states to filter by
+                * @param modules array list of module names to filter (optional, all modules
                 *   will be used by default)
                 * @return array list of filtered module names
                 */
@@ -450,12 +518,10 @@ window.mediaWiki = new ( function( $ ) {
                        return list;
                }
 
-               this.filter_ = filter;
-
                /**
                 * Executes a loaded module, making it ready to use
                 *
-                * @param string module name to execute
+                * @param module string module name to execute
                 */
                function execute( module ) {
                        if ( typeof registry[module] === 'undefined' ) {
@@ -470,16 +536,18 @@ window.mediaWiki = new ( function( $ ) {
                        // Add style sheet to document
                        if ( typeof registry[module].style === 'string' && registry[module].style.length ) {
                                $( 'head' )
-                                       .append( '<style type="text/css">' + registry[module].style + '</style>' );
-                       } else if ( typeof registry[module].style === 'object' 
-                               && !( registry[module].style instanceof Array ) ) 
+                                       .append( mediaWiki.html.element( 'style',
+                                               { type: "text/css" },
+                                               new mediaWiki.html.Cdata( registry[module].style )
+                                       ) );
+                       } else if ( typeof registry[module].style === 'object'
+                               && !( registry[module].style instanceof Array ) )
                        {
                                for ( var media in registry[module].style ) {
-                                       $( 'head' ).append(
-                                               '<style type="text/css" media="' + media + '">' +
-                                               registry[module].style[media] +
-                                               '</style>'
-                                       );
+                                       $( 'head' ).append( mediaWiki.html.element( 'style',
+                                               { type: 'text/css', media: media },
+                                               new mediaWiki.html.Cdata( registry[module].style[media] )
+                                       ) );
                                }
                        }
                        // Add localizations to message system
@@ -493,8 +561,8 @@ window.mediaWiki = new ( function( $ ) {
                                // Run jobs who's dependencies have just been met
                                for ( var j = 0; j < jobs.length; j++ ) {
                                        if ( compare(
-                                               filter( 'ready', jobs[j].dependencies ), 
-                                               jobs[j].dependencies ) ) 
+                                               filter( 'ready', jobs[j].dependencies ),
+                                               jobs[j].dependencies ) )
                                        {
                                                if ( typeof jobs[j].ready === 'function' ) {
                                                        jobs[j].ready();
@@ -506,9 +574,9 @@ window.mediaWiki = new ( function( $ ) {
                                // Execute modules who's dependencies have just been met
                                for ( r in registry ) {
                                        if ( registry[r].state == 'loaded' ) {
-                                               if ( compare( 
-                                                       filter( ['ready'], registry[r].dependencies ), 
-                                                       registry[r].dependencies ) ) 
+                                               if ( compare(
+                                                       filter( ['ready'], registry[r].dependencies ),
+                                                       registry[r].dependencies ) )
                                                {
                                                        execute( r );
                                                }
@@ -532,12 +600,12 @@ window.mediaWiki = new ( function( $ ) {
                }
 
                /**
-                * Adds a dependencies to the queue with optional callbacks to be run 
+                * Adds a dependencies to the queue with optional callbacks to be run
                 * when the dependencies are ready or fail
                 *
-                * @param mixed string moulde name or array of string module names
-                * @param function ready callback to execute when all dependencies are ready
-                * @param function error callback to execute when any dependency fails
+                * @param dependencies string module name or array of string module names
+                * @param ready function callback to execute when all dependencies are ready
+                * @param error function callback to execute when any dependency fails
                 */
                function request( dependencies, ready, error ) {
                        // Allow calling by single module name
@@ -545,7 +613,7 @@ window.mediaWiki = new ( function( $ ) {
                                dependencies = [dependencies];
                                if ( dependencies[0] in registry ) {
                                        for ( var n = 0; n < registry[dependencies[0]].dependencies.length; n++ ) {
-                                               dependencies[dependencies.length] = 
+                                               dependencies[dependencies.length] =
                                                        registry[dependencies[0]].dependencies[n];
                                        }
                                }
@@ -553,8 +621,8 @@ window.mediaWiki = new ( function( $ ) {
                        // Add ready and error callbacks if they were given
                        if ( arguments.length > 1 ) {
                                jobs[jobs.length] = {
-                                       'dependencies': filter( 
-                                               ['undefined', 'registered', 'loading', 'loaded'], 
+                                       'dependencies': filter(
+                                               ['undefined', 'registered', 'loading', 'loaded'],
                                                dependencies ),
                                        'ready': ready,
                                        'error': error
@@ -609,7 +677,7 @@ window.mediaWiki = new ( function( $ ) {
                        queue = [];
                        // After document ready, handle the batch
                        if ( !suspended && batch.length ) {
-                               // Always order modules alphabetically to help reduce cache 
+                               // Always order modules alphabetically to help reduce cache
                                // misses for otherwise identical content
                                batch.sort();
                                // Build a list of request parameters
@@ -641,10 +709,10 @@ window.mediaWiki = new ( function( $ ) {
                                                { 'modules': groups[group].join( '|' ), 'version': formatVersionNumber( version ) }, base
                                        );
                                }
-                               // Clear the batch - this MUST happen before we append the 
-                               // script element to the body or it's possible that the script 
-                               // will be locally cached, instantly load, and work the batch 
-                               // again, all before we've cleared it causing each request to 
+                               // Clear the batch - this MUST happen before we append the
+                               // script element to the body or it's possible that the script
+                               // will be locally cached, instantly load, and work the batch
+                               // again, all before we've cleared it causing each request to
                                // include modules which are already loaded
                                batch = [];
                                // Asynchronously append a script tag to the end of the body
@@ -654,7 +722,8 @@ window.mediaWiki = new ( function( $ ) {
                                                requests[r] = sortQuery( requests[r] );
                                                // Build out the HTML
                                                var src = mediaWiki.config.get( 'wgLoadScript' ) + '?' + $.param( requests[r] );
-                                               html += '<script type="text/javascript" src="' + src + '"></script>';
+                                               html += mediaWiki.html.element( 'script',
+                                                       { type: 'text/javascript', src: src }, '' );
                                        }
                                        return html;
                                }
@@ -668,7 +737,7 @@ window.mediaWiki = new ( function( $ ) {
                };
 
                /**
-                * Registers a module, letting the system know about it and its 
+                * Registers a module, letting the system know about it and its
                 * dependencies. loader.js files contain calls to this function.
                 */
                this.register = function( module, version, dependencies, group ) {
@@ -701,19 +770,19 @@ window.mediaWiki = new ( function( $ ) {
                                // Allow dependencies to be given as a single module name
                                registry[module].dependencies = [dependencies];
                        } else if ( typeof dependencies === 'object' || typeof dependencies === 'function' ) {
-                               // Allow dependencies to be given as an array of module names 
+                               // Allow dependencies to be given as an array of module names
                                // or a function which returns an array
                                registry[module].dependencies = dependencies;
                        }
                };
 
                /**
-                * Implements a module, giving the system a course of action to take 
-                * upon loading. Results of a request for one or more modules contain 
+                * Implements a module, giving the system a course of action to take
+                * upon loading. Results of a request for one or more modules contain
                 * calls to this function.
                 */
                this.implement = function( module, script, style, localization ) {
-                       // Automaically register module
+                       // Automatically register module
                        if ( typeof registry[module] === 'undefined' ) {
                                mediaWiki.loader.register( module );
                        }
@@ -721,19 +790,19 @@ window.mediaWiki = new ( function( $ ) {
                        if ( typeof script !== 'function' ) {
                                throw new Error( 'script must be a function, not a ' + typeof script );
                        }
-                       if ( typeof style !== 'undefined' 
-                               && typeof style !== 'string' 
-                               && typeof style !== 'object' ) 
+                       if ( typeof style !== 'undefined'
+                               && typeof style !== 'string'
+                               && typeof style !== 'object' )
                        {
                                throw new Error( 'style must be a string or object, not a ' + typeof style );
                        }
-                       if ( typeof localization !== 'undefined' 
-                               && typeof localization !== 'object' ) 
+                       if ( typeof localization !== 'undefined'
+                               && typeof localization !== 'object' )
                        {
                                throw new Error( 'localization must be an object, not a ' + typeof localization );
                        }
-                       if ( typeof registry[module] !== 'undefined' 
-                               && typeof registry[module].script !== 'undefined' ) 
+                       if ( typeof registry[module] !== 'undefined'
+                               && typeof registry[module].script !== 'undefined' )
                        {
                                throw new Error( 'module already implemeneted: ' + module );
                        }
@@ -741,8 +810,8 @@ window.mediaWiki = new ( function( $ ) {
                        registry[module].state = 'loaded';
                        // Attach components
                        registry[module].script = script;
-                       if ( typeof style === 'string' 
-                               || typeof style === 'object' && !( style instanceof Array ) ) 
+                       if ( typeof style === 'string'
+                               || typeof style === 'object' && !( style instanceof Array ) )
                        {
                                registry[module].style = style;
                        }
@@ -750,9 +819,9 @@ window.mediaWiki = new ( function( $ ) {
                                registry[module].messages = localization;
                        }
                        // Execute or queue callback
-                       if ( compare( 
-                               filter( ['ready'], registry[module].dependencies ), 
-                               registry[module].dependencies ) ) 
+                       if ( compare(
+                               filter( ['ready'], registry[module].dependencies ),
+                               registry[module].dependencies ) )
                        {
                                execute( module );
                        } else {
@@ -763,16 +832,16 @@ window.mediaWiki = new ( function( $ ) {
                /**
                 * Executes a function as soon as one or more required modules are ready
                 *
-                * @param mixed string or array of strings of modules names the callback 
+                * @param dependencies string or array of strings of modules names the callback
                 *   dependencies to be ready before
                 * executing
-                * @param function callback to execute when all dependencies are ready (optional)
-                * @param function callback to execute when if dependencies have a errors (optional)
+                * @param ready function callback to execute when all dependencies are ready (optional)
+                * @param error function callback to execute when if dependencies have a errors (optional)
                 */
                this.using = function( dependencies, ready, error ) {
                        // Validate input
                        if ( typeof dependencies !== 'object' && typeof dependencies !== 'string' ) {
-                               throw new Error( 'dependencies must be a string or an array, not a ' + 
+                               throw new Error( 'dependencies must be a string or an array, not a ' +
                                        typeof dependencies )
                        }
                        // Allow calling with a single dependency as a string
@@ -802,24 +871,24 @@ window.mediaWiki = new ( function( $ ) {
                /**
                 * Loads an external script or one or more modules for future use
                 *
-                * @param {mixed} modules either the name of a module, array of modules, 
+                * @param modules mixed either the name of a module, array of modules,
                 *   or a URL of an external script or style
-                * @param {string} type mime-type to use if calling with a URL of an 
-                *   external script or style; acceptable values are "text/css" and 
-                *   "text/javascript"; if no type is provided, text/javascript is 
+                * @param type string mime-type to use if calling with a URL of an
+                *   external script or style; acceptable values are "text/css" and
+                *   "text/javascript"; if no type is provided, text/javascript is
                 *   assumed
                 */
                this.load = function( modules, type ) {
                        // Validate input
                        if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
-                               throw new Error( 'dependencies must be a string or an array, not a ' + 
+                               throw new Error( 'dependencies must be a string or an array, not a ' +
                                        typeof dependencies )
                        }
                        // Allow calling with an external script or single dependency as a string
                        if ( typeof modules === 'string' ) {
                                // Support adding arbitrary external scripts
-                               if ( modules.substr( 0, 7 ) == 'http://' 
-                                       || modules.substr( 0, 8 ) == 'https://' ) 
+                               if ( modules.substr( 0, 7 ) == 'http://'
+                                       || modules.substr( 0, 8 ) == 'https://' )
                                {
                                        if ( type === 'text/css' ) {
                                                $( 'head' )
@@ -827,7 +896,8 @@ window.mediaWiki = new ( function( $ ) {
                                                        .attr( 'href', modules ) );
                                                return true;
                                        } else if ( type === 'text/javascript' || typeof type === 'undefined' ) {
-                                               var script = '<script type="text/javascript" src="' + modules + '"></script>';
+                                               var script = mediaWiki.html.element( 'script',
+                                                       { type: 'text/javascript', src: modules }, '' );
                                                if ( ready ) {
                                                        $( 'body' ).append( script );
                                                } else {
@@ -869,8 +939,8 @@ window.mediaWiki = new ( function( $ ) {
                /**
                 * Changes the state of a module
                 *
-                * @param mixed module string module name or object of module name/state pairs
-                * @param string state string state name
+                * @param module string module name or object of module name/state pairs
+                * @param state string state name
                 */
                this.state = function( module, state ) {
                        if ( typeof module === 'object' ) {
@@ -888,20 +958,111 @@ window.mediaWiki = new ( function( $ ) {
                /**
                 * Gets the version of a module
                 *
-                * @param string module name of module to get version for
+                * @param module string name of module to get version for
                 */
                this.version = function( module ) {
                        if ( module in registry && 'version' in registry[module] ) {
                                return formatVersionNumber( registry[module].version );
                        }
                        return null;
-               }
+               };
 
                /* Cache document ready status */
 
                $(document).ready( function() { ready = true; } );
        } )();
 
+       /** HTML construction helper functions */
+       this.html = new ( function () {
+               function escapeCallback( s ) {
+                       switch ( s ) {
+                               case "'":
+                                       return '&#039;';
+                               case '"':
+                                       return '&quot;';
+                               case '<':
+                                       return '&lt;';
+                               case '>':
+                                       return '&gt;';
+                               case '&':
+                                       return '&amp;';
+                       }
+               }
+
+               /**
+                * Escape a string for HTML. Converts special characters to HTML entities.
+                * @param s The string to escape
+                */
+               this.escape = function( s ) {
+                       return s.replace( /['"<>&]/g, escapeCallback );
+               };
+
+               /**
+                * Wrapper object for raw HTML passed to mediaWiki.html.element().
+                */
+               this.Raw = function( value ) {
+                       this.value = value;
+               };
+
+               /**
+                * Wrapper object for CDATA element contents passed to mediaWiki.html.element()
+                */
+               this.Cdata = function( value ) {
+                       this.value = value;
+               };
+
+               /**
+                * Create an HTML element string, with safe escaping.
+                *
+                * @param name The tag name.
+                * @param attrs An object with members mapping element names to values
+                * @param contents The contents of the element. May be either:
+                *    - string: The string is escaped.
+                *    - null or undefined: The short closing form is used, e.g. <br/>.
+                *    - this.Raw: The value attribute is included without escaping.
+                *    - this.Cdata: The value attribute is included, and an exception is
+                *      thrown if it contains an illegal ETAGO delimiter.
+                *      See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2
+                *
+                * Example:
+                *    var h = mediaWiki.html;
+                *    return h.element( 'div', {},
+                *        new h.Raw( h.element( 'img', {src: '<'} ) ) );
+                * Returns <div><img src="&lt;"/></div>
+                */
+               this.element = function( name, attrs, contents ) {
+                       var s = '<' + name;
+                       for ( attrName in attrs ) {
+                               s += ' ' + attrName + '="' + this.escape( attrs[attrName] ) + '"';
+                       }
+                       if ( typeof contents == 'undefined' || contents === null ) {
+                               // Self close tag
+                               s += '/>';
+                               return s;
+                       }
+                       // Regular open tag
+                       s += '>';
+                       if (typeof contents === 'string') {
+                               // Escaped
+                               s += this.escape( contents );
+                       } else if ( contents instanceof this.Raw ) {
+                               // Raw HTML inclusion
+                               s += contents.value;
+                       } else if ( contents instanceof this.Cdata ) {
+                               // CDATA
+                               if ( /<\/[a-zA-z]/.test( contents.value ) ) {
+                                       throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
+                               }
+                               s += contents.value;
+                       } else {
+                               throw new Error( 'mw.html.element: Invalid type of contents' );
+                       }
+                       s += '</' + name + '>';
+                       return s;
+               };
+       } )();
+
+
        /* Extension points */
 
        this.legacy = {};