MozileCore.js

Summary

This file defines the Mozile object, which contains most of Mozile's methods and properties. Also defines the MozileCommand and MozileCommandList objects, from which all other commands are derived, and extends the Node and Selection objects in simple ways.

Version: 0.7.0

Author: James A. Overton


Class Summary
Mozile
MozileCommandList
MozileCommand

/* ***** BEGIN LICENSE BLOCK *****
 * Licensed under Version: MPL 1.1/GPL 2.0/LGPL 2.1
 * Full Terms at http://mozile.mozdev.org/license.html
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Playsophy code (www.playsophy.com).
 *
 * The Initial Developer of the Original Code is Playsophy
 * Portions created by the Initial Developer are Copyright (C) 2002-2003
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *	James A. Overton <james@overton.ca>
 *
 * ***** END LICENSE BLOCK ***** */


/** Mozile Core  
 * @fileoverview This file defines the Mozile object, which contains most of Mozile's methods and properties. Also defines the MozileCommand and MozileCommandList objects, from which all other commands are derived, and extends the Node and Selection objects in simple ways.
 * 
 * @link http://mozile.mozdev.org 
 * @author James A. Overton <james@overton.ca>
 * @version 0.7.0
 */







/**** GLOBALS ****/

// Declare the XUL namespace, which is used for the creation of elements in the Mozile toolbar.
var XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

// A regular expression that matches any non-white-space character.
var matchNonWS = /\S/;





/**** OBJECTS ****/


/** Mozile Object -
 * Creates the Mozile object, which is used for almost all of Mozile's functionality. A single Mozile object is associated with a single Document.
 * @constructor
 *
 * Configuration String format (some options may conflict): "root='path/to/mozile', mode=XHTML, namespace='http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd', keyboardShortcuts=true, toolbarUpdateFrequency=2, warnBeforeUnload=true, debugLevel=0"
 *
 * @param optionsString An option string, formatted like the configuration strings for modules and commands.
 * @return A string indicating success or an error.
 */
function Mozile(optionsString) {

	var optionsArray = this.parseOptions(optionsString);
	//dumpArray(optionsArray);

	/**** DEFINITIONS ****/
	// Define the properties for this object.

	/** Mozile - Version -
	 * The version of the Mozile Core code.
	 */
	this.version = "0.7.0";

	/** Mozile - Root -
	 * The root directory for this Mozile installation. This file "MozileCore.js" should be found at "[root]core/MozileCore.js".
	 */
	this.root = null;

	/** Mozile - Mode -
	 * The mode controls which tools are used to manipulate the document. The options are "HTML", "XHTML", and "XML". Each of the three modes has its quirks.
	 * TODO: Currently, Mozile makes no use of the mode.
	 */
	this.mode = "XHTML";
	
	/** Mozile - Namespace -
	 * A proper namespace for the document. This will be used when creating elements.
	 * TODO: Currently, Mozile makes no use of the namespace.
	 */
	this.namespace = null;
	
	/** Mozile - Debug Level -
	 * An integer indicating how verbose debugging should be. 0 means only critical errors are shown. Higher values mean more verbose debugging: 1="very important", 2="important", 3="normal", 4="not important"
	 */
	this.debugLevel = 0;
	
	/** Mozile - Script List -
	 * An array containing the id's of all the scripts which have been loaded using this.loadScript().
	 */
	this.scriptList = new Array("Mozile-Core-MozileCore.js");
	
	/** Mozile - Link List -
	 * An array containing the id's of all the links which have been loaded using this.loadLink().
	 */
	this.linkList = new Array();
	
	/** Mozile - Style List -
	 * An array containing the id's of all the style elements which have been added to the document by Mozile.
	 */
	this.styleList = new Array();
	
	/** Mozile - Module List -
	 * An array containing all the loaded modules and their versions. The key for this array is the module name, and the values are the module version strings. 
	 */
	this.moduleList = new Array();
	
	/** Mozile - Command List -
	 * An array containing all of the commands (but no the command lists) registered with this Mozile object. The keys are the id strings the commands, and values the command objects.
	 */
	this.commandList = new Array();
	
	/** Mozile - Accelerator List -
	 * An array containing all of the keyboard shortcuts (accelerators) for Mozile commands. The keys are strings of the format "control-shift-f", while the values are the command objects.
	 */
	this.acceleratorList = new Array();
	
	/** Mozile - Editor List -
	 * An array containing entries for every editor in the document.
	 */
	this.editorList = new Array();
	
	/** Mozile - Current Editor -
	 * When an editor takes the focus, it sets currentEditor to its root element.
	 */
	this.currentEditor = null;
	
	/** Mozile - Style Sheet -
	 * The CSSStyleSheet object which is manipulated to add the XBL bindings which control the Mozile toolbar and the Mozile editors
	 */
	this.styleSheet = null;
	
	/** Mozile - Toolbar -
	 * Mozile's toolbar element.
	 */
	this.toolbar = null;
	
	/** Mozile - Toolbar Update -
	 * An integer specifying how often the toolbar should update. 0=Never, 1=Rarely, 2=Often.
	 */
	this.toolbarUpdateFrequency = 2;
	
	/** Mozile - First Show Toolbar -
	 *  A flag which is "true" if the Mozile toolbar has never been shown (i.e. Mozile.showToolbar() has never been called).
	 */
	this.firstToolbarShow = true;
	
	/** Mozile - Last Focus Node -
	 * Stores the last focused node. If the current focused node is the same, then the toolbar does not have to be updated.
	 */
	this.lastFocusNode = null;
	
	/** Mozile - Last IP -
	 * Stores the last insertion point.
	 */
	this.lastIP = new Array("","","");
	
	/** Mozile - Key Counter -
	 * Counts the number of keypresses which lead to character insertion.
	 */
	this.keyCounter = 0;
	
	/** Mozile - Max Key Count -
	 * When keyCounter > maxKeyCount the state of the document is stored.
	 */
	this.maxKeyCount = 20;
	
	/** Mozile - Keyboard Shortcuts -
	 * A Boolean value. When it is true, Mozile tries to use keyboard shortcuts for commands.
	 */
	this.keyboardShortcuts = true;
	
	/** Mozile - Changes Saved -
	 * This is true after any of the to[Format]() methods have been called, and false after any other command.
	 */
	this.changesSaved = true;
	
	/** Mozile - Operating System -
	 * Mozile checks the UserAgent string for the browser, and tries to determine what operating system the browser is running under. Can be "Linux", "Windows", or "Mac". Note that the UserAgent can be spoofed, so this is not entirely reliable.
	 */
	this.operatingSystem = null;

	
	/**** VALIDATION ****/
	// Check to make sure that values are valid, then set them.
	
	// Check to see if a valid root was provided.
	if(optionsArray["root"]) {
		this.root = optionsArray["root"];
	}
	else {
		return "Error initializing Mozile object -- invalid root provided.";
	}
	
	// Check to see if a valid mode was provided.
	if(optionsArray["mode"] && (optionsArray["mode"]=="HTML" || optionsArray["mode"]=="XHTML" || optionsArray["mode"]=="XML") ) {
		this.mode = optionsArray["mode"];
	}
	
	// Check to see if a valid namespace was provided.
	if(optionsArray["namespace"]) {
		this.namespace = optionsArray["namespace"];
	}
	
	// Check to see if debugLevel has been overridden.
	if(optionsArray["debugLevel"]) {
		this.debugLevel = optionsArray["debugLevel"];
	}

	// Check to see if toolbarUpdateFrequency has been overridden.
	if(optionsArray["toolbarUpdateFrequency"]) {
		this.toolbarUpdateFrequency = optionsArray["toolbarUpdateFrequency"];
	}	

	// Check to see if keyboardShortcuts has been overridden.
	if(optionsArray["keyboardShortcuts"] && optionsArray["keyboardShortcuts"]=="false") {
		this.keyboardShortcuts = false;
	}	
	
	// Set up onbeforeunload event handler.
	if(optionsArray["warnBeforeUnload"] && optionsArray["warnBeforeUnload"]=="false") {
		// Do nothing.
		window.onbeforeunload = function() { return; }
	}
	else {
		// Default option: Warn the user of any unsaved changes.
		window.onbeforeunload = function() {
			return "There are unsaved changes in this document. Changes will be lost if you navigate away from this page.";
		}
	}
	
	// Try to determine the user's operating system.
	var userAgent = navigator.userAgent.toLowerCase();
	if(userAgent.indexOf("windows") >= 0) this.operatingSystem = "Windows";
	if(userAgent.indexOf("linux") >= 0) this.operatingSystem = "Linux";
	if(userAgent.indexOf("macintosh") >= 0) this.operatingSystem = "Mac";


	// If everything has worked out, return the success message.
	return "Success";
	
}




/** Mozile - Debug -
 * The stub of a debugging tool. If the debugFlag is set, it sends alerts. Otherwise, it does nothing. More extensive debugging can be provided by modules.
 * 
 * @param details An array of information. The first two fields are "filename", and "location" (usually a function name). Fancier debugging functions might use more fields. 
 * @param level An integer describing the importance of the debugging message. 0="critical", 1="very important", 2="important", 3="normal", 4="not important".
 * @param message A string containing the debugging message.
 * @return Always true.
 */
function mozileDebug(details, level, message) {
	// Check against the debug level that's been set
	if(!this.debugLevel || level > this.debugLevel) return true;
	
	// otherwise, add it to the debugging array
	var date = new Date();
	mozileDebugList.push([date.toLocaleString(), details, level, message]);
	
	return true;
}

// Create the global debug list
var mozileDebugList = new Array();

// Add the mozileDebug function as a method of the Mozile object.
Mozile.prototype.debug = mozileDebug;


/** Mozile - Load Script -
 * Creates an X/HTML script element, at the appropriate place in the document, which loads the JavaScript file referred to by "src". This will be inside the "head" element for X/HTML, or under the document element for XML. These tags are automatically removed before Mozile saves the document. Each tag also has an id, which is added to the this.scriptList array.
 * 
 * @param src A relative or absolute path to the file which should be loaded.
 * @param id A unique id string for this tag. The usual format is "Mozile-ModuleName-FileName.js".
 * @return Always true.
 */
Mozile.prototype.loadScript = function(src, id) {
	var f = new Array("core/MozileCore.js","Mozile.loadScript()");
	this.debug(f,3,"Loading script "+ src +" with id "+ id);

	var scriptTag ="";

	// Check to see if this is an X/HTML document.
	if(document.documentElement.tagName.toLowerCase()=="html") {
		// Build the required script tag in the default namespace
		scriptTag = document.createElement("script");
		// Try to get the head tag
		var head = document.getElementsByTagName("head")[0];
	}
	// If it's not X/HTML, assume it's generic XML
	else {
		// Build the required script tag in the XHTML namespace
		scriptTag = document.createElementNS(XHTMLNS,"script");
	}

	// Finish creating the tag.
	scriptTag.setAttribute("id", id);
	scriptTag.setAttribute("src", src);
	scriptTag.setAttribute("type","application/x-javascript");

	// Insert the tag into the appropriate place in the document.
	if(head) {
		head.appendChild(scriptTag);
	}
	else {
		document.documentElement.insertBefore(scriptTag,document.documentElement.firstChild);
	}
	
	// If successful, add the id to the this.scriptList array.
	this.scriptList.push(id);
	
	return true;
}




/** Mozile - Unload Script -
 * Removes all X/HTML script tags with the given id.
 * 
 * @param id The id of the tag to be removed.
 * @return Always true.
 */
Mozile.prototype.unloadScript = function(id) {
	var f = new Array("core/MozileCore.js","Mozile.unloadScript()");
	this.debug(f,3,"Unloading script "+id);
	
	// Loop over all script elements and remove any which match the id.
	// Yes, we could getElementById, but this method will catch any accidental duplicates, and it shouldn't be much slower.
	var scripts = document.getElementsByTagName("script");
	for(i=0;i<scripts.length;i++) {
		if(scripts[i].getAttribute("id") == id) {
			scripts[i].parentNode.removeChild(scripts[i]);
		}
	}
	
	return true;
}




/** Mozile - Load Link -
 * Creates an X/HTML link element, at the appropriate place in the document, which loads the CSS file referred to by "src". This will be inside the "head" element for X/HTML, or under the document element for XML. These tags are automatically removed before Mozile saves the document. Each tag also has an id, which is added to the this.linkList array.
 * 
 * @param href A relative or absolute path to the file which should be loaded.
 * @param id A unique id string for this tag. The usual format is "Mozile-ModuleName-FileName.css".
 * @return Always true.
 */
Mozile.prototype.loadLink = function(href, id) {
	var f = new Array("core/MozileCore.js","Mozile.loadLink()");
	this.debug(f,3,"Loading link "+ href +" with id "+ id);

	var linkTag;

	// Check to see if this is an X/HTML document.
	if(document.documentElement.tagName.toLowerCase()=="html") {
		// Build the required script tag in the default namespace
		linkTag = document.createElement("link");
		// Try to get the head tag
		var head = document.getElementsByTagName("head")[0];
	}
	// If it's not X/HTML, assume it's generic XML
	else {
		// Build the required script tag in the XHTML namespace
		linkTag = document.createElementNS(XHTMLNS,"link");
	}

	// FInish building the element.	
	linkTag.setAttribute("id", id);
	linkTag.setAttribute("href", href);
	linkTag.setAttribute("rel", "stylesheet");
	linkTag.setAttribute("type", "text/css");
	
	// Insert the element into the appropriate place in the document. 
	if(head) {
		head.appendChild(linkTag);
	}
	else {
		document.documentElement.insertBefore(linkTag,document.documentElement.firstChild);
	}
	
	// If successful, add the id to the this.scriptList array.
	this.linkList.push(id);
	
	return true;
}




/** Mozile - Unload Link -
 * Removes all X/HTML links tags which file referred to by "id".
 * 
 * 
 * @param id The id of the tag to be removed.
 * @return Always true.
 */
Mozile.prototype.unloadLink = function(id) {
	var f = new Array("core/MozileCore.js","Mozile.unloadLink()");
	this.debug(f,3,"Unloading link "+id);
	
	// Loop over all link elements and remove any which match the id.
	// Yes, we could getElementById, but this method will catch any accidental duplicates, and it shouldn't be much slower.
	var links = document.getElementsByTagName("link");
	for(i=0;i<links.length;i++) {
		if(links[i].getAttribute("id") == id) {
			links[i].parentNode.removeChild(links[i]);
		}
	}

	return true;
}






/** Mozile - Load Module -
 * Loads a module into the document. By default, modules are stored under the "modules" directory in a directory named moduleName, with a main script called "moduleName.js". So the src of the script tag would be "[this.root]modules/moduleName/moduleName.js". If a version is included the src becomes "[this.root]modules/moduleName-version/moduleName.js". If a path is given, it is added to the beginning: "[path]moduleName/moduleName.js".
 * 
 * @param path Optional. A string naming the path to use.
 * @return False if the module is already loaded, true otherwise.
 */
Mozile.prototype.loadModule = function(configString) {
	var f = new Array("core/MozileCore.js","Mozile.loadModule()");
	this.debug(f,2,"Loading module using configuration string: "+configString);

	// Parse the configString.
	var configArray = this.parseConfig(configString);
	
	// Check to see if this module has already been loaded. If so, throw an error and return.
	if(this.moduleList[configArray['name']]) {
		this.debug(f,0,"Error: Module "+ configArray['name'] +" is already loaded. It will not be loaded again.");
		return false;
	}
	
	// Build the src string.
	var src="";
	if(configArray['remotePath']) { src = configArray['remotePath']; }
	else { src = this.root +"modules/";  }
	src = src + configArray['name'];
	if(configArray['remoteVersion']) src = src +"-"+ configArray['remoteVersion'];
	src = src +"/"+ configArray['name'] +".js"
	this.debug(f,2,"Source for module: "+src);

	// Build the id string.
	var id = "Mozile-"+ configArray['name'] +"-"+ configArray['name'] +".js";
	
	// Load the script.
	this.loadScript(src,id);

	// Add an entry to the this.moduleList array. The module will register itself and set the version string.	
	this.moduleList[configArray['name']]="unknown version";
	
	return true;
}




/** Mozile - Register Module -
 * Registers the version of a module with the moduleList. This function must be called by all Mozile modules when they have finished their initialization.
 * 
 * @param moduleName A string naming the module to be loaded, i.e. "CopyCutPaste".
 * @param versionString A string naming the version which has been loaded. Should be formatted as X.Y.Z where X,Y, and Z are integers.
 * @return Always true.
 */
Mozile.prototype.registerModule = function(moduleName, versionString) {
	var f = new Array("core/MozileCore.js","Mozile.registerModule()");
	this.debug(f,3,"Registering module "+ moduleName +" version "+ versionString);
	
	// Set the version string in the this.moduleList array.
	this.moduleList[moduleName]=versionString;
	
	return true;
}



/** Mozile - Unregister Module -
 * Removes the given module from the moduleList.
 * 
 * @param moduleName A string naming the module to be removed, i.e. "CopyCutPaste". 
 * @return Always true.
 */
Mozile.prototype.unregisterModule = function(moduleName) {
	var f = new Array("core/MozileCore.js","Mozile.unregisterModule()");
	this.debug(f,3,"Unregistering module "+ moduleName);
	
	// If the module isn't loaded, it can't be unregistered.
	if(!this.moduleIsLoaded(moduleName)) return false;

	// Rebuild the moduleList without the unregistered module.
	var newModuleList = new Array();
	for(key in this.moduleList) {
		if(key != moduleName) newModuleList[key] = this.moduleList[key];
	}
	this.moduleList = newModuleList;
	
	return true;
}



/** Mozile - Module Is Loaded-
 * Checks to see if the given module has been loaded. The simple case checks just by the name. An optional requirements string can specify sophisticated requirements, such as minimum, maximum, and excluded versions.
 * 
 * @param moduleName A string naming the module to be checked, i.e. "CopyCutPaste".
 * @param requirements Optional. A string describing the requirements for this module, using the same format as the module configuration string.
 * @return True if the module is loaded, false otherwise.
 */
Mozile.prototype.moduleIsLoaded = function(moduleName, requirements) {
	var f = new Array("core/MozileCore.js","Mozile.moduleIsLoaded()");
	this.debug(f,3,"Checking for module "+ moduleName +" with requirements "+ requirements);

	// If a requirements string has been given, call the checkRequirements function.	
	if(requirements) {
		var loadedVersion = this.moduleList[moduleName];
		if(this.checkRequirements(loadedVersion,requirements)) return true;
		else return false;
	}
	// Otherwise, simply check to see if the moduleName is a key in the moduleList array.
	else {
		if(this.moduleList[moduleName]) return true;
		else return false;
	}
	
}




/** Mozile - Check Requirements -
 * Check to see if a given version string meets the requirements specified in a requirement string (like a configuration string). Requirements include: (zero or one:) requiredVersion, minVersion, maxVersion, (zero or more:) notVersion.
 * 
 * @param versionString A string which will be checked against the requirements. The format should be X.Y.Z, with X,Y,Z integers.
 * @param requirements A string describing the requirements for this module, using the same format as the module configuration string.
 * @return True if requirements are met, false otherwise.
 */
Mozile.prototype.checkRequirements = function(versionString, requirements) {
	var f = new Array("core/MozileCore.js","Mozile.checkRequirements()");
	this.debug(f,3,"Checking requirements for version "+ versionString +" against requirements "+ requirements);

	// Parse the requirements string into an options array.	
	var reqArray = this.parseOptions(requirements);

	// Parse the versionString into integers X.Y.Z
	var parseVersion = /(\d*)\.(\d*)\.(\d*)/;
	var version = parseVersion.exec(versionString);
	
	// If the version doesn't match then it is invalid, so return false.
	if(!version) return false;
	
	// If a particular version is required, then only that precise version will do.
	if(reqArray['requireVersion']) {
		if(versionString == reqArray['requireVersion']) return true;
		else return false;
	}
	
	// Check if minimum version is satisfied.
	if(reqArray['minVersion']) {
		var minVersion = parseVersion.exec(reqArray['minVersion']);
		if(minVersion) {
			if(version[1] < minVersion[1]) return false;
			if(version[2] < minVersion[2]) return false;
			if(version[3] < minVersion[3]) return false;
		}
	}
	
	// Check if maximum version is satisfied.
	if(reqArray['maxVersion']) {
		var maxVersion = parseVersion.exec(reqArray['maxVersion']);
		if(minVersion) {
			if(version[1] > maxVersion[1]) return false;
			if(version[2] > maxVersion[2]) return false;
			if(version[3] > maxVersion[3]) return false;
		}
	}
	
	// Check every entry on the notVersion array to make sure the versionString doesn't match.
	if(reqArray['notVersion']) {
		for(var n=0; n < reqArray['notVersion'].length; n++) {
			if(versionString == reqArray['notVersion'][n]) return false;
		}
	}
	
	// If all the tests were passed, then the versionString meets the requirements.
	return true;
}



/** Mozile - Parse Config -
 * Parses a configuration string into a configuration array.
 * 
 * @param configString The configuration string, following the standard format (see "mozile.js" under "Declare Modules", or the Mozile.createCommand() method in this file).
 * @return A module configuration array.
 */
Mozile.prototype.parseConfig = function(configString) {
	var f = new Array("core/MozileCore.js","Mozile.parseConfig()");
	this.debug(f,3,"Parsing configuration string: "+configString);

	var configArray = new Array();

	// Include the raw configString in the new array.	
	configArray['configString']=configString;
	
	// Grab the first word in the string. This will be the name	of the module or command.
	var firstWord = /\s*(\w*)/;
	var arr = firstWord.exec(configString);
	var name = arr[1]; // the match we want
	configArray['name']=name;

	// Now check for a colon ":". If none is found, then there are no options to consider, so return the array.
	if(configString.indexOf(":")==-1) {
		return configArray;
	}

	// If there is a colon, take the part after the colon as the optionsString.
	var optionString = configString.substring(configString.indexOf(":")+1, configString.length);

	// Get the options array for this options string.
	var optionArray = this.parseOptions(optionString);
	
	// Join the two arrays (a bit of a hack, but concat wasn't working for me)
	for(key in optionArray) {
		configArray[key]=optionArray[key];
	}
	
	return configArray;
}



/** Mozile - Parse Options -
 * Parses an option string into an option array.
 * 
 * @param optionString The option string, following the standard format for module or command options (see "mozile.js" under "Declare Modules", or the Mozile.createCommand() method in this file).
 * @return A module options array.
 */
Mozile.prototype.parseOptions = function(optionString) {
	var f = new Array("core/MozileCore.js","Mozile.parseOptions()");
	this.debug(f,3,"Parsing option string: "+optionString);

	var optionArray = new Array();
	
	// Split the optionsString at commas.
	var options = optionString.split(",");
	
	// Now parse each option into key-value pairs, and add them to the optionArray as optionArray[key]=value.
	// This regular expression matches two formats: with='spa ces' and without=spaces.
	var parseOption = /(\S*)='(.*)'|(\S*)=(\S*)/;
	var option, arr;
	for(o in options) {
		option = options[o];
		arr = parseOption.exec(option);
		// make sure the matches are good
		if(arr) {
			var key,val;
			// If neither of the first two groups match, use the second two groups.
			if(!arr[1] && !arr[2]) key=arr[3],val=arr[4];
			else key=arr[1],val=arr[2];
			// Special case: the notVersions get their own array
			if(key=="notVersion") {
				if(!optionArray['notVersion']) optionArray['notVersion'] = new Array();
				optionArray['notVersion'].push(val);
			}
			// General case: add key and value to the optionArray.
			else {
				optionArray[key]=val;
			}
		}
	}

	return optionArray;
}





/** Mozile - Create Command -
 * This method creates a Mozile command object from a configuration string. It works almost identically to the MozileCommand.List.createCommand() method. However, this method is usually used only to create a root command list, which contains all the other commands for the Mozile toolbar. For this reason, it does not register commands.
 * 
 * Format (particular command objects may have other options): "MozileCommandObject: id=CommandId, label='Command Label', tooltip='Command Tool Tip', image='/path/to/image.file', accesskey=C, accelerator='Meta-C', buttonPosition=0, menuPosition=0"
 *
 * @param configString A configuration string, following the above format.
 * @return The newly created command object.
 */
Mozile.prototype.createCommand = function(configString) {
	var f = new Array("core/MozileCore.js","Mozile.createCommand()");
	this.debug(f,3,"Creating command "+ configString);

	// Parse the configuration string.
	var configArray = this.parseConfig(configString);
	
	var name = configArray['name'];
	var command;
	
	// Define the command creation string. I.e. "command = new MozileCommand(configArray)"
	var create = "command = new "+name+"(configArray)";
	
	// Evaluate the command creation string.
	eval(create);
	
	// This method does not register commands
	//this.registerCommand(command);
	
	return command;

}




/** Mozile - Register Command -
 * Registers a command or command list object, adding it to the this.commandList array.
 * 
 * @param command The command object.
 * @return True when successful, false otherwise.
 */
Mozile.prototype.registerCommand = function(command) {
	var f = new Array("core/MozileCore.js","Mozile.registerCommand()");
	this.debug(f,3,"Registering command "+ command +" with id "+ command.id);

	var id = command.id;
	// If a command with that id is already registered, return false.
	if(this.commandIsRegistered(id)) return false;
	// Otherwise, add the command object to the commandList, with it id as the key.
	else {
		this.commandList[id] = command;
		if(command.accelerator) {
			var accel = command.accelerator;
			if(this.operatingSystem=="Mac") {
				accel = accel.replace("Command", "Meta");
			}
			else {
				accel = accel.replace("Command", "Control");
			}
			this.acceleratorList[accel] = command;
		}
		return true;
	}

}



/** Mozile - Unregister Command -
 * Unregisters a command or command list object by removing it from the commandList array.
 * 
 * @param id The id of the command. 
 * @return Always true.
 */
Mozile.prototype.unregisterCommand = function(id) {
	var f = new Array("core/MozileCore.js","Mozile.unregisterCommand()");
	this.debug(f,3,"Unregistering command "+ id);

	// If there is no such command, it can't be unregistered.
	if(!this.commandIsRegistered(id)) return true;
	else {
		this.commandList[id] = null;
		return true;
	}

}



/** Mozile - Command Is Registered -
 * Checks to see if a command with the given id has been registered.
 * 
 * @param id The id of the command. 
 * @return True if a command with that id is registered, false otherwise.
 */
Mozile.prototype.commandIsRegistered = function(id) {
	var f = new Array("core/MozileCore.js","Mozile.commandIsRegistered()");
	this.debug(f,3,"Checking for command "+ id);

	if(this.commandList[id]) return true;
	else return false;

}



/** Mozile - Execute Command -
 * Calls the command() method of the command object corresponding to the given id.
 * 
 * @param id The id of the node which triggered the command. 
 * @param event The event which triggered this command. 
 * @return Always true.
 */
Mozile.prototype.executeCommand = function(id, event) {
	var f = new Array("core/MozileCore.js","Mozile.executeCommand()");
	this.debug(f,3,"Executing command "+ id +" "+ event);

	// Remove the "-Button" or "-Menuitem" part of the id, if there is one.
	var cleanId = /(.*)(\-Button|\-Menuitem)$/;
	var result = cleanId.exec(id);
	//alert(result);
	var commandId;
	if(result) commandId = result[1];
	else commandId = id;

	if(!this.commandIsRegistered(commandId)) return false;
	// If the command id is registered, call the command method of the corresponding command object.
	else {
		var command = this.commandList[commandId];
		
		// If the keyCounter is not 0, then something has changed since the last command, so store the state.
		if(this.keyCounter != 0) this.storeState(command);
		
		// Execute the command
		result = command.command(event);
		
		// If there is some result, and this is not an undo or redo command, store the state after the change.
		if(result && command.id!="Mozile-Undo" && command.id!="Mozile-Redo") this.storeState(command);

		this.updateToolbar(true);
		return true;
	}

}




/** Mozile - Handle Keypress -
 * Decides what action to take for the given "keypress" event, and calls the appropriate function.
 * 
 * @param event The keypress event to be handled. 
 * @return True when successful, false otherwise.
 */
Mozile.prototype.handleKeypress = function(event) {
	var f = new Array("core/MozileCore.js","Mozile.handleKeypress()");
	this.debug(f,3,"Handling keypress event "+ event);
	
	// Ignore the function keys (i.e. F7).
	if(event.keyCode >= 112 & event.keyCode <= 135) return true;
	
	var selection = window.getSelection();

	// Make sure we have a focusNode
	if(!selection.focusNode) return true;


	// When the cursor has moved, update the toolbar.
	if(event.keyCode >=33 && event.keyCode <= 40) {	
		// The cursor can get stuck against an empty node when moving left or right. When this happens, we can help it along.
		if(event.keyCode == 37 || event.keyCode == 39) {
			// Check to see if the cursor hasn't moved since the last keypress
			if(selection.focusNode == this.lastIP[0] && selection.focusOffset == this.lastIP[1] && event.keyCode == this.lastIP[2]) {
				// Set the proper direction
				var direction="next";
				if(event.keyCode==37) direction="previous";
				this.debug(f,3,"Cursor seems stuck. Jumping to "+direction+" IP.");		
				// Seek a new IP
				var node = this.seekTextNode(direction, selection.focusNode);
				// Collapse the selection into the new IP.
				if(direction=="next") selection.collapse(node, 0);
				else selection.collapse(node, node.textContent.length);
				event.stopPropagation();
				event.preventDefault();
			}
			// Store the current IP
			else {
				this.lastIP = [selection.focusNode, selection.focusOffset, event.keyCode];
				//alert(this.lastIP);
			}
			return true;
		}
	
		// TODO: Updating every time might be too slow. Make this an option.
		if(this.toolbarUpdateFrequency==2) {
			this.updateToolbar();
		}
		return true;
	}
	
	// Check for keyboard shortcuts triggering commands.
	if(this.keyboardShortcuts && (event.ctrlKey || event.metaKey)) {
		var accel = "";
		if(event.metaKey)  accel = accel + "Meta-";
		if(event.ctrlKey)  accel = accel + "Control-";
		if(event.altKey)   accel = accel + "Alt-";
		if(event.shiftKey) accel = accel + "Shift-";
		accel = accel + String.fromCharCode(event.charCode).toUpperCase();
		//alert(accel);
		//dumpArray(this.acceleratorList);
		if(this.acceleratorList[accel]) {
			this.executeCommand(this.acceleratorList[accel].id, event);
			this.updateToolbar();
			event.stopPropagation();
  		event.preventDefault();
			return true;
		}
		else {
			return true;
		}
	}


	// Check that the parent node is modifiable and accepts input
	try {
		var userModify = document.defaultView.getComputedStyle(selection.focusNode.parentNode, '').getPropertyValue("-moz-user-modify").toLowerCase();
		var userInput = document.defaultView.getComputedStyle(selection.focusNode.parentNode, '').getPropertyValue("-moz-user-input").toLowerCase();
		if(userModify=="read-only" || userInput=="disabled") return true;
	} catch(e) {
		alert("Bad selection? "+e+"\n"+selection.focusNode);
	}

	
	// Handle delete and backspace keys.
	if(event.keyCode == event.DOM_VK_BACK_SPACE) {
		this.deletion("previous");
		event.stopPropagation();
		return true;
	}
	if(event.keyCode == event.DOM_VK_DELETE){
		this.deletion("next");
		event.stopPropagation();
		return true;
	}

	
	// Check to see if the node uses CSS white-space="pre".
	var whiteSpace = document.defaultView.getComputedStyle(selection.focusNode.parentNode, '').getPropertyValue("white-space").toLowerCase();
	

	// Handle the enter key. If the white-space is "pre", insert a newline, and otherwise split the current block element.
	if(event.keyCode == event.DOM_VK_ENTER || event.keyCode == event.DOM_VK_RETURN){
		if(whiteSpace=="pre") {
			this.insertString("\n");
		}
		else {
			this.splitBlock();
		}
		this.storeState("Enter Key");
		event.stopPropagation();
		return true;
	}

	// Handle the tab key by inserting a tab.
	if(event.keyCode == event.DOM_VK_TAB) {
		this.insertString("\t");
		this.keyCounter++;
		if(this.keyCounter > this.maxKeyCount) this.storeState("Typing");
		event.stopPropagation();
  	event.preventDefault();
		return true;
	}
	
	// Handle all other non-command keystrokes by inserting a string with the value of the character code.
	if(!event.ctrlKey && !event.metaKey) {
		this.insertString(String.fromCharCode(event.charCode));
		this.keyCounter++;
		if(this.keyCounter > this.maxKeyCount) this.storeState("Typing");
		event.stopPropagation();
  	event.preventDefault();
	}

	return true;
}


/** Mozile - Handle Keyup -
 * Decides what action to take for the given "keyup" event, and calls the appropriate function.
 * 
 * @param event The keypress event to be handled. 
 * @return True when successful, false otherwise.
 */
Mozile.prototype.handleKeyup = function(event) {
	var f = new Array("core/MozileCore.js","Mozile.handleKeyup()");
	this.debug(f,3,"Handling keyup event "+ event);
	
	// if an arrow key has been release, update the toolbar.
	if(event.keyCode >=33 && event.keyCode <= 40) {
		mozile.updateToolbar();
		return true;
	}

	// Handle delete and backspace keys.
	if(event.keyCode == event.DOM_VK_BACK_SPACE) {
		this.storeState("Backspace Key");
		return true;
	}
	if(event.keyCode == event.DOM_VK_DELETE){
		this.storeState("Delete Key");
		return true;
	}

	
	return true;
}



/** Mozile - Insert String -
 * Inserts a string at the current selection index. If the selection is not collapsed, then the selection is deleted before the new string is inserted.
 * 
 * @param string The string to be inserted. 
 * @return True when string has been inserted, false otherwise.
 */
Mozile.prototype.insertString = function(string) {
	var f = new Array("core/MozileCore.js","Mozile.insertString()");
	this.debug(f,3,"Inserting string "+ string);
	
	// Get the current selection
	var selection = window.getSelection();
	// If it is not collapsed, delete the contents of the selection
	if(!selection.isCollapsed) {
		this.deletion("next");
	}
	
	// Get the focusNode, and make sure it's a text node
	var focusNode = selection.focusNode;
	if(focusNode.nodeType != 3) {
		// if it's not a text node, try its first child...
		selection.extend(focusNode.firstChild,0);
		focusNode = selection.focusNode;
		// if we still can't find it, fail
		if(focusNode.nodeType != 3) {
			this.debug(f,0,"This node is not a text node! " + focusNode);
			return false;
		}
	}
	
	// Insert the string at the focusOffset
	focusNode.insertData(selection.focusOffset, string);
	
	// Try to move the selection forward by the length of the string.
	try {
		selection.extend(selection.focusNode,selection.focusOffset+string.length);
	}
	catch(e) {
		alert("Error in insertString "+e);
	}

	// Collapse the selection again.
	selection.collapseToEnd();

	return true;
}



/** Mozile - Insert Fragment -
 * Inserts all the children of a document fragment or an element at the current selection index. If the selection is not collapsed, then the selection is deleted before the fragment is inserted.
 * 
 * @param fragment Either a document fragment, or an element with child nodes to be cloned and inserted.
 * @return True when the fragment has been inserted, false otherwise.
 */
Mozile.prototype.insertFragment = function(fragment) {
	var f = new Array("core/MozileCore.js","Mozile.insertFragment()");
	this.debug(f,3,"Inserting fragment "+ fragment);
	
	// Get the current selection
	var selection = window.getSelection();

	// If it is not collapsed, delete the contents of the selection
	if(!selection.isCollapsed) {
		this.deletion("next");
	}
	
	// Get the focusNode, and make sure it's a text node
	var anchorNode = selection.anchorNode;
	if(anchorNode.nodeType != 3) {
		this.debug(f,0,"This node is not a text node! "+anchorNode);
		return false;
	}

	// Split the current text node to make room for the new nodes.
	selection.anchorNode.splitText(selection.anchorOffset);

	var parent = selection.anchorNode.parentNode;
	var next = selection.anchorNode.nextSibling;
	var children;

	// In both cases (document fragment or an element) and get the children.
	if(fragment.documentElement) children = fragment.documentElement.childNodes;
	else children = fragment.cloneNode(true).childNodes;

	// Clone each child node "deep" and insert it before the "next" node.
	var newNode;
	for(var i=0;i<children.length;i++) {
		newNode = children[i].cloneNode(true);
		parent.insertBefore(newNode, next);
	}
	
	// Move the selection
	selection.collapseToEnd();

	return true;
}




/** Mozile - Delete -
 * Deletes the current selection, or everything between the current and the previous insertion points. This will usually mean a single character, but *ML white-space rules can make it more complicated.
 * 
 * @param direction A string indicating where to delete. Can be "next" or "previous". 
 * @return True if successful, false otherwise.
 */
Mozile.prototype.deletion = function(direction) {
	var f = new Array("core/MozileCore.js","Mozile.deletion()");
	this.debug(f,3,"Deleting in direction "+ direction);
	
	// Get the current selection
	var selection = window.getSelection();
	var collapsed = selection.isCollapsed;
	
	// Handle the case of a collapsed selection (which is more complicated than a non-collapsed selection).
	if(collapsed) {
		// Get the next (or previous) insertion point, starting at the current selection.
		var arr = this.seekIP(direction, selection.focusNode, selection.focusOffset);
		
		// Check to make sure that the new insertion point node exists.
		if(!arr || !arr[0]) return false;
		
		// Check to make sure that the new insertion point node is marked modifiable
		var userModify = document.defaultView.getComputedStyle(arr[0].parentNode, '').getPropertyValue("-moz-user-modify").toLowerCase();
		var userInput = document.defaultView.getComputedStyle(arr[0].parentNode, '').getPropertyValue("-moz-user-input").toLowerCase();
		if(userModify=="read-only" || userInput=="disabled") return false;

		// Try to extend the selection to the new insertion point.
		try {
			selection.extend(arr[0],arr[1]);
		} 
		catch(e) {
			return false;
		}
	}
	
	// First get some information that we might want to check later.
	var range = selection.getRangeAt(0).cloneRange();
	var startBlock = range.startContainer.parentBlock;
	var endBlock = range.endContainer.parentBlock;

	// Now delete the contents of the selection
	if(startBlock==endBlock || !collapsed) {
		selection.deleteContents();
	}
	else {
		if(direction=="previous") {
			range.setStart(range.startContainer, range.startOffset+1);
		}
		//alert("Case "+ collapsed);
	}
	
	// If the selection is no longer in a text node, create a new one and select it.
	if(collapsed && selection.anchorNode.nodeType==1) {
		var text = document.createTextNode("");
		selection.anchorNode.appendChild(text);
		range.selectNode(text);
		range.collapse(true);
		selection.removeAllRanges();
		selection.addRange(range);
	}
	
	// If the start and end blocks are different, then we want to merge the endBlock with the startBlock.
	if(startBlock != endBlock ) {
		range.collapse(true);
		selection.removeAllRanges();
		selection.addRange(range);
		this.insertFragment(endBlock);
		endBlock.parentNode.removeChild(endBlock);
	}

	return true;
}




/** Selection Delete Contents -
 * An improved deletion function. Creates a range which encompasses the selction, collapses the selection, then deletes the contents of the range. This avoids some selection bugs I was having using selection.deleteFromDocument().
 * 
 * @return Always true.
 */
Selection.prototype.deleteContents = function() {

	// Create a new range.
	var range = document.createRange();

	// Set the start and end of the new range. This is a bit tricky, because the anchorNode might be after the focusNode, or before it, but the range's start has to be before its end.
	try {
		// case where anchor is before focus
		range.setStart(this.anchorNode, this.anchorOffset);
		range.setEnd(this.focusNode, this.focusOffset);
		this.collapseToEnd();
	} catch(e) {
		// case where focus is before anchor
		range.setEnd(this.anchorNode, this.anchorOffset);
		range.setStart(this.focusNode, this.focusOffset);
		this.collapseToStart();
	}
	
	// Do the deleting.
	range.deleteContents();

	return true;
}





/** Mozile - Split Block -
 * Splits the current block in two, creating a new block after the old one in the process. This is the usual behaviour of the enter key.
 * 
 * @return Always true.
 */
Mozile.prototype.splitBlock = function() {
	var f = new Array("core/MozileCore.js","Mozile.splitBlock()");
	this.debug(f,3,"Splitting block");

	// Get the selection, and delete any contents it might have.
	var selection = window.getSelection();
	if(!selection.isCollapsed) {
		selection.deleteContents();
	}
	
	// Store some information for later.	
	var range = selection.getRangeAt(0).cloneRange();
	var focusNode = selection.focusNode;
	var focusOffset = selection.focusOffset;
	var node = focusNode;
	var flag = false;
	var display;
	
	// Get the first ancestor element which is has CSS display=block.
	node = node.getParentBlock();

	// If the node has a parent, then split it.	
	if(node.parentNode) {
	
		// Select the whole node, then set the range's start to the selection's focus.
		range.selectNodeContents(node);
		var nodeString = range.toString();
		range.setStart(focusNode, focusOffset);
		
		// Make a new node with the same name as the block we're splitting.
		var newNode = document.createElement(node.nodeName);

		// Put the contents of the range into the newNode (this removes them from the old node).
		
		var rangeString = range.toString();
		var textNode = null;
		
		if(rangeString!="") {
			newNode.appendChild(range.extractContents());
		}
		else {
		}
		
		switch(rangeString) {
			// If the range includes the whole node, then 
			case nodeString:
				newNode.appendChild(document.createTextNode(""));
				node.parentNode.insertAfter(newNode, node);
				textNode = focusNode;
				break;
			
			// If the range is empty, give the new node an empty text node
			case "":
				textNode = document.createTextNode("");
				newNode.appendChild(textNode);
				node.parentNode.insertAfter(newNode, node);
				break;
			
			default:
				newNode.appendChild(range.extractContents());
				node.parentNode.insertAfter(newNode, node);
				textNode = this.seekTextNode("next", focusNode);
				break;
		}
		
		// Move the selection into the first insetion point in the new node
		try {
			selection.extend(textNode, 0);
			selection.collapseToEnd();
		}
		catch(e) {
			this.debug(f,3,"Major error splitting block: "+e);
			// do nothing
		}
		
	}
	
	
	this.debug(f,3,"Done splitting block");
	
	return true;


}




/** Mozile - Seek Insertion Point -
 * Finds the next (or previous) insertion point, starting at a given text node and offset. The concept of an insertion point is critical to word-processing, but does not map nicely onto any of the DOM concepts. An insertion point is the pair of a text node and offset within that node, where characters can be inserted. A text node with no contents is not a valid insertion point. Unless CSS white-space="pre", a text node which contains nothing but white-space characters is not a valid insertion point either.
 * 
 * @param direction A string indicating the direction of the search. Can be "next" or "previous". 
 * @param startNode The text node to begin the search in.
 * @param startOffset Integer. The offset within the startNode to begin the search at. 
 * @return An array, with the first entry being a text node, and the second being the offset in that node.
 */
Mozile.prototype.seekIP = function(direction, startNode, startOffset) {
	var f = new Array("core/MozileCore.js","Mozile.seekIP()");
	this.debug(f,3,"Seeking IP in direction "+ direction +" starting at "+startNode+" "+startOffset);
	
	// Create a new range.
	var range = document.createRange();
	var newNode;
	
	// If the startOffset is 0 and we are asked for the previous IP, call seekIP on the previous text node. If there is no such node inside the editor, return false.
	if(direction=="previous" && startOffset==0) {	
		newNode = this.seekTextNode(direction, startNode);
		if(newNode) return this.seekIP(direction, newNode, newNode.textContent.length);
		else return false;
	}

	// Try to select the startNode with the range.
	try {
		range.selectNode(startNode);
	}
	catch(e) {
		return false;
	}
	
	// Get the contents of the range as a string.
	var content = range.toString();
	
	// If the startOffset is equal to the length of the content string, and we are asked for the next IP, then call seekIP on the next text node. If there is no such node within the editor, return false
	if(direction=="next" && startOffset==content.length) {
		newNode = this.seekTextNode(direction, startNode);
		if(newNode) return this.seekIP(direction, newNode, 0);
		else return false;
	}
	

	var currentOffset;
	var wsFlag = false;			// this will be set to true once the first white-space character is envountered.

	// wsMode will be "normal", "no-wrap", or "pre". 
	var wsMode = document.defaultView.getComputedStyle(startNode.parentNode, '').getPropertyValue("white-space").toLowerCase();

	// Set the currentOffset.
	if(direction=="next") currentOffset = startOffset;
	else currentOffset = startOffset-1;
	var arr = new Array();
	
	// Now we move the currentOffset until we find a (non-white-space) character (in which case we return), or until we hit the edge of the text node (in which case we break out of the while loop).
	while(currentOffset >= 0 && currentOffset < content.length) {

		// If the white-space is "pre" or the character at the currentOffset is a non-whitespace character, return the node and offset.
		if(wsMode=="pre" || matchNonWS.test(content.charAt(currentOffset))) {

			arr[0] = startNode;
			// If we are inside a whitespace block, then we return the offset at the end of the block.
			if(wsFlag) {
				if(direction=="next") arr[1]=currentOffset;
				else arr[1]=currentOffset+1;
			}
			// Otherwise we return a slightly different offset.
			else {
				if(direction=="next") arr[1]=currentOffset+1;
				else arr[1]=currentOffset;
			}
			return arr;
		}
		// If the character at characterOffset is a whitespace character, then increment the currentOffset and run this loop again, but set the wsFlag to true.
		else {
			wsFlag = true;
			if(direction=="next") currentOffset++;
			else currentOffset--;
		}
	}
	
	// If we were in a whitespace block before we hit the boundary of the node, return the currentOffset.
	if(wsFlag) {
		arr[0] = startNode;
		if(direction=="next") arr[1]=currentOffset;
		else arr[1]=currentOffset+1;
		return arr;
	}

	// Otherwise, we need to call seekIP on another text node.
	newNode = this.seekTextNode(direction, startNode);
	if(newNode) {
		if(direction=="next") return this.seekIP(direction, newNode, 1);
		else return this.seekIP(direction, newNode, newNode.textContent.length);
	}


	// If we made it this far, then something is broken.
	this.debug(f,1,"Something broke! "+currentOffset+" "+content.length);
	return false;
}





/** Mozile - Seek Text Node -
 * Finds the next (or previous) text node which either has CSS white-space="pre" or contains some non-white-space characters. If there is not such node, we return false.
 * 
 * @param direction String. Can be "next" or "previous". 
 * @param startNode The text node to begin the search in.
 * @return The requested text node.
 */
Mozile.prototype.seekTextNode = function(direction, startNode) {
	var f = new Array("core/MozileCore.js","Mozile.seekTextNode()");
	this.debug(f,3,"Seeking Text Node in direction "+ direction +" starting at "+startNode);
	
	// Create the tree walker
	// TODO: First parameter should be the root element of the editor, not the document.
	var treeWalker = document.createTreeWalker(document, NodeFilter.SHOW_TEXT, null, false);
	
	// Loop until we find the startNode.
	while(treeWalker.currentNode != startNode) {
		treeWalker.nextNode();
	}
	
	// Get the next or previous node, as appropriate.
	var checkNode;
	if(direction=="next") checkNode=treeWalker.nextNode();
	else checkNode=treeWalker.previousNode();
	
	
	var wsMode;
	
	// Loop until a non-white-space or "pre" text node is reached
	while(checkNode) {
	
		// If the node contains non-white-space characters, then return it.
		if(matchNonWS.test(checkNode.textContent)) return checkNode;
		
		// Now check to see that the text node has CSS white-space="pre" or contains something other than white-space characters. If it does not, check the next node until you find some non-white-space characters. Then return.
		// wsMode will be "normal", "no-wrap", or "pre". 
		wsMode = document.defaultView.getComputedStyle(checkNode.parentNode, '').getPropertyValue("white-space").toLowerCase();
		if(wsMode=="pre") return checkNode;
		
		// If neither of this tests succeeds, then grab the next (or previous) node.
		if(direction=="next") checkNode=treeWalker.nextNode();
		else checkNode=treeWalker.previousNode();
	}
	
	// If we made it this far, then something is broken.
	this.debug(f,1,"Something broke!");
	return false;
}




/** Mozile - Create Toolbar -
 * Does all the work required to add the Mozile toolbar to the current document. This includes adding a new stylesheet to the document, and adding a CSS rule which associates an XBL binding with the body element (in the case of X/HTML), or a new <moziletoolbar/> element (which is inserted into an XML document).
 * 
 * @param type String. Selects the kind of toolbar to create. TODO: Currently has no effect. 
 * @return True if successful.
 */
Mozile.prototype.createToolbar = function(type) {
	var f = new Array("core/MozileCore.js","Mozile.createToolbar()");
	this.debug(f,3,"Creating toolbar "+ type);

	// Add basic style-sheet to the document.
	this.loadLink(this.root + "core/widgets.css", "Mozile-Core-widgets.css");
	
	// Create style element.
	var id = "Mozile-Core-StyleSheet";
	var styleTag, head;
	// Check to see if this is an X/HTML document.
	if(document.documentElement.tagName.toLowerCase()=="html") {
		styleTag = document.createElement("style");
		head = document.getElementsByTagName("head")[0];
	} 
	else {
		styleTag = document.createElementNS(XHTMLNS,"style");
	}
	styleTag.setAttribute("id", id);
	styleTag.setAttribute("type", "text/css");
	if(head) {
		head.appendChild(styleTag);
	}
	else {
		document.documentElement.insertBefore(styleTag,document.documentElement.firstChild);
	}
	this.styleList.push(id);
	
	// Grab the style-sheet for the new style tag, add it to the Mozile property styleSheet. This is tricker than it should be, since I couldn't find a way to go directly from the styleTag element to the styleSheet object which it creates.
	var sheets = document.styleSheets;
	var sheet;
	for(var i=0; i < sheets.length; i++) {
		if(sheets.item(i).ownerNode == styleTag) {
			sheet = sheets.item(i);
			break;
		}
	}
	this.debug(f,3,"Style sheet selected: "+ i +" "+ sheet +" "+ sheet.href);
	this.styleSheet = sheet;

	// Add a new rule to the end of the sheet which binds the toolbar to the body element.
	var rule;
	// Handle the X/HTML case, where the toolbar is attached to the body element.
	if(document.documentElement.tagName.toLowerCase()=="html") {
		rule = "body { -moz-binding: url(" + this.root +"core/widgets.xbl#toolbar); }";
	}
	// Handle the XML case, where the toolbar is attached to a new moziletoolbar element, inserted near the top of the document.
	else {
		rule = "moziletoolbar { -moz-binding: url(" + this.root +"core/widgets.xbl#toolbar); }";
		document.documentElement.insertBefore(document.createElement("moziletoolbar"), document.documentElement.firstChild);
	}
	
	// Insert the rule.
	sheet.insertRule(rule, sheet.cssRules.length);
	

	// Create base command list object, and populate it.
	this.rootCommandList = mozile.createCommand("MozileCommandList: id=Mozile-RootList, label='Mozile Root List'");

	// Create the basic Mozile command list.
	var mozileList = this.rootCommandList.createCommand("MozileCommandList: id=Mozile-firstList, label='Mozile First List', image='"+this.root+"images/link.png', buttonPosition=0, menuPosition=0");
	
	var about = mozileList.createCommand("MozileCommand: id=Mozile-About, label='About Mozile'");
	about.command = function(event) {
		window.open(mozile.root+"core/about.xul", "About Mozile", "centerscreen,chrome,resizable=yes");
	}
	
	var accel = mozileList.createCommand("MozileCommand: id=Mozile-Accelerators, label='Keyboard Shortcuts'");
	accel.command = function(event) {
		if(mozile.keyboardShortcuts) {
			var message = "Mozile defines the following keyboard shortcuts:";
			var accels = mozile.acceleratorList;
			for(key in accels) {
				message = message +"\n"+accels[key].label+" => "+key;
			}
			alert(message);
		} 
		else {
			alert("Mozile keyboard shortcuts have been turned off.");
		}
	}
	
	var debug = mozileList.createCommand("MozileCommand: id=Mozile-Debug, label=Debug");
	debug.command = function(event) {
		window.open(mozile.root+"core/debug.xul", "Mozile Debugging", "centerscreen,chrome,resizable=yes");
	}
	
	//var test = mozileList.createCommand("MozileCommand: id=Mozile-Test, label='Testing Tools'");
	//test.command = function(event) {
	//	window.open(mozile.root+"testing/index.html", "Mozile Testing", "");
	//}
	
	var report = mozileList.createCommand("MozileCommand: id=Mozile-ReportBugs, label='Report a Bug'");
	report.command = function(event) {
		window.open("http://mozile.mozdev.org/bugs.html", "Mozile Bugs", "");
	}
	
	var home = mozileList.createCommand("MozileCommand: id=Mozile-Home, label='Mozile Home'");
	home.command = function(event) {
		window.open("http://mozile.mozdev.org", "Mozile Home", "");
	}
	
	var help = mozileList.createCommand("MozileCommand: id=Mozile-Help, label='Help'");
	help.command = function(event) {
		window.open(mozile.root+"docs/index.html", "Mozile Help", "");
	}

	var save = this.rootCommandList.createCommand("MozileCommand: id=Mozile-SaveToDialog, label=Save, tooltip='Save to a dialog', accelerator='Command-S', image='"+this.root+"images/save.png'");
	save.command = function(event) {
		window.open(mozile.root+"core/source.xul", "Mozile Save", "centerscreen,chrome,resizable=yes");
	}

	return true;
}





/** Mozile - Initialize Toolbar -
 * Does some basic setup on the toolbar, which can't be done until after all modules have loaded. 
 * TODO: Make the basic toolbar useful, with a Help and About command.
 * 
 * @return True if successful.
 */
Mozile.prototype.initializeToolbar = function() {
	var f = new Array("core/MozileCore.js","Mozile.initializeToolbar()");
	this.debug(f,3,"Initializing toolbar");

	// Create the command box.	
	commands.createBox();
	
	// Add all of the children of the command box to the toolbar.
	var children = commands.box.childNodes;
	for(var i=0; i<children.length; i++) {
		this.toolbar.appendChild(children[i]);
	}
	
	return true;

}




/** Mozile - Hide Toolbar -
 * Hides the Mozile toolbar.
 * 
 * @return True if successful.
 */
Mozile.prototype.hideToolbar = function() {
	var f = new Array("core/MozileCore.js","Mozile.hideToolbar()");
	this.debug(f,3,"Hiding toolbar");

	this.toolbar.collapsed = true;
}




/** Mozile - Show Toolbar -
 * Shows (unhides) the Mozile toolbar. If this is the first time that the toolbar has been shown, then it calls this.initializeToolbar().
 * 
 * @return True if successful.
 */
Mozile.prototype.showToolbar = function() {
	var f = new Array("core/MozileCore.js","Mozile.showToolbar()");
	this.debug(f,3,"Showing toolbar. First time? " + this.firstToolbarShow);
	
	if(this.firstToolbarShow) {
		this.initializeToolbar();
		this.storeState("Initial state");
		this.firstToolbarShow=false;
	}
	this.toolbar.collapsed = false;
}



/** Mozile - Show Toolbar -
 * Updates all of the commands in the commandList so that they will reflect the current selection.
 * 
 * @param force Optional Boolean, forces update when "true" and toolbarUpdateFrequency > 0.
 * @return True if successful.
 */
Mozile.prototype.updateToolbar = function() {
	var f = new Array("core/MozileCore.js","Mozile.updateToolbar()");
	this.debug(f,3,"Updating toolbar");
	
	if(this.toolbarUpdateFrequency==0) return true;
	
	var force = false;
	if(arguments.length > 0) {
		force = arguments[0];
	}
	
	var selection = window.getSelection();
	
	// If the update is not being forced, then check to see if it is required.
	if(!force) {
		// Don't update if the node has not changed.
		if(selection.focusNode == this.lastFocusNode) {
			this.debug(f,4,"Toolbar update not required");
			return true;
		}	
	}
	
	this.debug(f,3,"Updating toolbar");
	this.lastFocusNode = selection.focusNode;
	
	// Update all the commands in the command list
	for(var command in this.commandList) {
		try {
			this.commandList[command].update();
		} catch(e) {
			//alert("Error "+e+"\n"+command);
		}
		
	}
	
	return true;
}



/** Mozile - Create Editor -
 * Creates a Mozile editor in the document using it's id. Two CSS rules are added to this.styelSheet, the first of which binds an XBL widget to the element and captures the focus, while the second tells all children to ignore the focus.
 * 
 * @param id String. The id of the element to be made an editor. 
 * @param options String. A list of options which will be used in this editor. TODO: No options are currently implemented.
 * @return True if successful.
 */
Mozile.prototype.createEditor = function(id, options) {
	var f = new Array("core/MozileCore.js","Mozile.createEditor()");
	this.debug(f,3,"Creating editor "+ id +" "+ options);

	// add a new rule to the end of the sheet which binds the editor to the element with the given id
	var rule1 = "#"+ id +" { -moz-binding: url(" + this.root +"core/widgets.xbl#editor); -moz-user-focus: normal;	-moz-user-modify: read-write; }";
	var rule2 = "#"+ id +" * { -moz-user-focus: ignore; }";
	
	this.styleSheet.insertRule(rule1, this.styleSheet.cssRules.length);
	this.styleSheet.insertRule(rule2, this.styleSheet.cssRules.length);

	return true;
}




/** Mozile - Create Editors -
 * Creates multiple Mozile editors in the document, using a CSS selector.
 * 
 * @param selector String. The id of the element to be made an editor. 
 * @param options String. A list of options which will be used in this editor.
 * @return True if successful.
 */
Mozile.prototype.createEditors = function(selector, options) {
	var f = new Array("core/MozileCore.js","Mozile.createEditors()");
	this.debug(f,3,"Creating editor "+ selector +" "+ options);

	// add a new rule to the end of the sheet which binds the editor to the elements which match the selector
	var rule1 = selector +" { -moz-binding: url(" + this.root +"core/widgets.xbl#editor); -moz-user-focus: normal;	-moz-user-modify: read-write; }";
	var rule2 = selector +" * { -moz-user-focus: ignore; }";
	
	this.styleSheet.insertRule(rule1, this.styleSheet.cssRules.length);
	this.styleSheet.insertRule(rule2, this.styleSheet.cssRules.length);

	return true;
}



/** Mozile - Register Editor -
 * Adds an editor's root node to the this.editorList. This methodis called by the contructor of new editors.
 * 
 * @param element The root element of the new editor.
 * @return True if successful.
 */
Mozile.prototype.registerEditor = function(element) {
	var f = new Array("core/MozileCore.js","Mozile.registerEditor()");
	this.debug(f,3,"Registering editor "+ element);

	this.editorList.push(element);

}



/** Mozile - Set Current Editor -
 * Sets the this.ecurrentEditor to the given element.
 * 
 * @param element The root element of the new currentEditor.
 * @return True if successful.
 */
Mozile.prototype.setCurrentEditor = function(element) {
	var f = new Array("core/MozileCore.js","Mozile.setCurrentEditor()");
	this.debug(f,3,"Setting current editor "+ element);

	this.currentEditor = element;

}


/** Mozile - Clean Up -
 * Removes all of the elements which Mozile has inserted into the document.
 * 
 * @param element The root element from which all other elements will be removed.
 * @return The cleaned up element.
 */
Mozile.prototype.cleanUp = function(element) {
	var f = new Array("core/MozileCore.js","Mozile.cleanUp()");
	this.debug(f,3,"Cleaning up element "+element);

	var i,j=0;
	
	// Loop over all script elements and remove any which match the id.
	// Yes, we could getElementById, but this method will catch any accidental duplicates, and it shouldn't be much slower.
	var scripts = element.getElementsByTagName("script");
	for(i=0; i < scripts.length; i++) {
		for(j=0; j < this.scriptList.length; j++) {
			if(scripts[i].getAttribute("id") == this.scriptList[j]) {
				scripts[i].parentNode.removeChild(scripts[i]);
			}
		}
	}
	
	// Do the same for links.
	var links = element.getElementsByTagName("link");
	for(i=0; i < links.length; i++) {
		for(j=0; j < this.linkList.length; j++) {
			if(links[i].getAttribute("id") == this.linkList[j]) {
				links[i].parentNode.removeChild(links[i]);
			}
		}
	}
	
	// Do the same for style elements.
	var styles = element.getElementsByTagName("style");
	for(i=0; i < styles.length; i++) {
		for(j=0; j < this.styleList.length; j++) {
			if(styles[i].getAttribute("id") == this.styleList[j]) {
				styles[i].parentNode.removeChild(styles[i]);
			}
		}
	}
	
	// Get rid of any moziletoolbar elements.
	var moziletoolbars = element.getElementsByTagName("moziletoolbar");
	for(i=0; i < moziletoolbars.length; i++) {
		moziletoolbars[i].parentNode.removeChild(moziletoolbar[i]);
	}
	
	return element;
}


/** Mozile - To HTML -
 * Extract the contents of the document as HTML, first cleaning up any mess that Mozile has made.
 * 
 * @return String of the serialized XML contents.
 */
Mozile.prototype.toHTML = function() {
	var f = new Array("core/MozileCore.js","Mozile.toHTML()");
	this.debug(f,3,"Rendering document to XML string");

	var serializer = new XMLSerializer;
	
	// Make a copy of the document to work on.
	var newDoc = document.documentElement.cloneNode(true);
	
	newDoc = this.cleanUp(newDoc);
	
	// Serialize the contents.
	var contents = serializer.serializeToString(newDoc);
	
	// Changes have been seen.
	this.changesSaved = true;
	
	return contents;
}



/** Mozile - To XML -
 * Extract the contents of the document as XML, first cleaning up any mess that Mozile has made.
 * 
 * @return String of the serialized XML contents.
 */
Mozile.prototype.toXML = function() {
	var f = new Array("core/MozileCore.js","Mozile.toXML()");
	this.debug(f,3,"Rendering document to XML string");

	var serializer = new XMLSerializer;
	
	// Make a copy of the document to work on.
	var newDoc = document.documentElement.cloneNode(true);
	
	newDoc = this.cleanUp(newDoc);
	
	// Serialize the contents.
	var contents = serializer.serializeToString(newDoc);
	
	// Changes have been seen.
	this.changesSaved = true;
	
	return mozileHTML2xhtml(contents);
}




/** Mozile - Editor To HTML -
 * Extract the contents of the current editor as HTML, first cleaning up any mess that Mozile has made.
 * 
 * @return String of the serialized XML contents.
 */
Mozile.prototype.editorToHTML = function() {
	var f = new Array("core/MozileCore.js","Mozile.editorToHTML()");
	this.debug(f,3,"Rendering current editor "+ this.currentEditor +" to XML string");

	if(!this.currentEditor) {
		this.debug(f,0,"No current editor to serialize to XML!");
		return false;
	}

	var serializer = new XMLSerializer;
	
	// Make a copy of the document to work on.
	var editor = this.currentEditor.cloneNode(true);
	
	editor = this.cleanUp(editor);
	
	// Serialize the contents.
	var contents = serializer.serializeToString(editor);
	
	// Changes have been seen.
	this.changesSaved = true;

	return contents;
}




/** Mozile - Editor To XML -
 * Extract the contents of the current editor as XML, first cleaning up any mess that Mozile has made.
 * 
 * @return String of the serialized XML contents.
 */
Mozile.prototype.editorToXML = function() {
	var f = new Array("core/MozileCore.js","Mozile.editorToXML()");
	this.debug(f,3,"Rendering current editor "+ this.currentEditor +" to XML string");

	if(!this.currentEditor) {
		this.debug(f,0,"No current editor to serialize to XML!");
		return false;
	}

	var serializer = new XMLSerializer;
	
	// Make a copy of the document to work on.
	var editor = this.currentEditor.cloneNode(true);
	
	editor = this.cleanUp(editor);
	
	// Serialize the contents.
	var contents = serializer.serializeToString(editor);
	
	// Changes have been seen.
	this.changesSaved = true;
	
	return mozileHTML2xhtml(contents);
}




// Convert HTML to xhtml compatible.
// All tag names are converted to lower case.
// Converts a whole html document or just a fragment.
// The string to be converted must have been generated by the XML Serializer.
//
function mozileHTML2xhtml( h ) {
	var TAG = /[A-Z]/;
	var c, i, x, startTag, inTag, endTag;
	x = "";
	startTag = endTag = inTag = false;
	for( i=0; i<h.length; i++ ){
		c=h[i];
		if (c=='<'){
			startTag = true;
			inTag = endTag = false;
			x+=c;
		}else if (startTag) {
			if ( TAG.test(c) ) {
				x+=c.toLowerCase();
				inTag = true;
			}else{
				x+=c;
				if ( c == '/' )
					endTag = true;
			} 
			startTag = false;
		}else if (inTag) {
			if ( TAG.test(c) ) {
				x+=c.toLowerCase();
			}else {
				inTag = false;
				x+=c;
			} 
		}else if (endTag) {
			if ( TAG.test(c) ) {
				x+=c.toLowerCase();
				inTag = true;
			}else {
				x+=c;
			} 
			endTag = false;
		}else {
			x+=c;
		}
	}
	return x;
}





/** Mozile - Store State -
 * This is the stub of a command used in the UndoRedo module. It does nothing except rest the keyCounter.
 * 
 * @return Always true.
 */
Mozile.prototype.storeState = function(command) {
	var f = new Array("core/MozileCore.js","Mozile.storeState()");
	this.debug(f,3,"Storing current state");

	// Changes have been made.
	this.changesSaved = false;
	
	// Reset the key counter.
	this.keyCounter = 0;
	
	return true;

}


/** Mozile - Store Selection -
 * Store the selection in a recoverable form.
 * 
 * @return An array with all the details required to restore the selection.
 */
Mozile.prototype.storeSelection = function() {
	var f = new Array("core/MozileCore.js","Mozile.storeSelection()");
	this.debug(f,3,"Storing current selection");

	var selection = window.getSelection();
	
	if(!selection.anchorNode || !selection.focusNode) {
		this.debug(f,2,"No selection to store!");
		return false;
	}

	var elementList, children;

	// Get the name of the element containing the anchor node, and the index of the anchor node within the children of that element.
	var anchorElement;
	var anchorNodeIndex = "none";
	if(selection.anchorNode.nodeType==3) {
		anchorElement = selection.anchorNode.parentNode;
		children = anchorElement.childNodes;
		for(i=0; i<children.length; i++) {
			if(children[i]==selection.anchorNode) {
				anchorNodeIndex = i;
				break;
			}
		}
	}
	else {
		anchorElement = selection.anchorNode;
	} 
	var anchorNodeOffset = selection.anchorOffset;	
	var anchorElementName = anchorElement.nodeName;
	
	// Get the index of the anchorElement in the list of all elements with the same name.
	var anchorElementIndex;
	elementList = document.getElementsByTagName(anchorElementName);
	for(var i=0; i<elementList.length; i++) {
		if(elementList[i]==anchorElement) {
			anchorElementIndex = i;
			break;
		}
	}
	
	// If the selection is collapsed, the focus and anchor are the same.
	if(selection.isCollapsed) {
		focusElementName = anchorElementName;
		focusElementIndex = anchorElementIndex;
		focusNodeIndex = anchorNodeIndex;
		focusNodeOffset = anchorNodeOffset;
	}
	else {
		// Get the name of the element containing the focus node, and the index of the focus node within the children of that element.
		var focusElement;
		var focusNodeIndex = "none";
		if(selection.focusNode.nodeType==3) { 
			focusElement = selection.focusNode.parentNode;
			children = focusElement.childNodes;
			for(i=0; i<children.length; i++) {
				if(children[i]==selection.focusNode) {
					focusNodeIndex = i;
					break;
				}
			}
		}
		else {
			focusElement = selection.focusNode;
		} 
		var focusNodeOffset = selection.focusOffset;	
		var focusElementName = focusElement.nodeName;
		
		// Get the index of the focusElement in the list of all elements with the same name.
		var focusElementIndex;
		elementList = document.getElementsByTagName(focusElementName);
		for(i=0; i<elementList.length; i++) {
			if(elementList[i]==focusElement) {
				focusElementIndex = i;
				break;
			}
		}

	}
		
	
	var selectionArray = new Array(anchorElementName, anchorElementIndex, anchorNodeIndex, anchorNodeOffset, focusElementName, focusElementIndex, focusNodeIndex, focusNodeOffset);
	
	return selectionArray;
}



/** Mozile - Restore Selection -
 * Restores the selection using the output from this.storeSelection.
 * 
 * @return True if successful.
 */
Mozile.prototype.restoreSelection = function(selectionArray) {
	var f = new Array("core/MozileCore.js","Mozile.restoreSelection()");
	this.debug(f,3,"Restoring current selection using "+selectionArray);

	if(!selectionArray) {
		this.debug(f,2,"No selectionArray to restore!");
		return false;
	}

	// Extract the data in a readable format.
	var anchorElementName  = selectionArray[0];
	var anchorElementIndex = selectionArray[1];
	var anchorNodeIndex    = selectionArray[2];
	var anchorNodeOffset   = selectionArray[3];
	var focusElementName   = selectionArray[4];
	var focusElementIndex  = selectionArray[5];
	var focusNodeIndex     = selectionArray[6];
	var focusNodeOffset    = selectionArray[7];


	// Find the anchor and focus nodes.
	var anchorNode;
	if(anchorNodeIndex!="none") {
		anchorNode = document.getElementsByTagName(anchorElementName)[anchorElementIndex].childNodes[anchorNodeIndex];
	}
	else {
		anchorNode = document.getElementsByTagName(anchorElementName)[anchorElementIndex].firstChild;
	}

	var focusNode;
	if(focusNodeIndex!="none") {
		focusNode = document.getElementsByTagName(focusElementName)[focusElementIndex].childNodes[focusNodeIndex];
	}
	else {
		focusNode = document.getElementsByTagName(focusElementName)[focusElementIndex].firstChild;
	}
	
	// Build a range, first assuming the anchor is before the focus. If that fails, try the opposite way.
	var range = document.createRange();
	try {
		range.setStart(anchorNode, anchorNodeOffset);
		range.setEnd(focusNode, focusNodeOffset);
	}
	catch(e) {
		range = document.createRange();
		range.setEnd(anchorNode, anchorNodeOffset);
		range.setStart(focusNode, focusNodeOffset);
	}

	// Restore the selection.
	var selection = window.getSelection();
	selection.removeAllRanges();
	selection.addRange(range);
	return true;

}














/****  Mozile Command Object ****/

/** Mozile Command Object -
 * Creates a Mozile command object, which controls the button, menuitem, and code associated with a Mozile editing command.
 * @constructor
 *
 * Configuration String format (particular command objects may have other options): "MozileCommandObject: id=CommandId, label='Command Label', tooltip='Command Tool Tip', image='/path/to/image.file', accesskey=C, accelerator='Command-C', buttonPosition=0, menuPosition=0, debugLevel=0"
 * Accelerator format must follow this sequence: "Command-Meta-Control-Alt-Shift-Key". Mozile will check the UserAgent string for the browser, and replace "Command" with "Control" on Linux and Windows, or "Meta" on Macintosh. One of "Command", "Control" or "Meta" must be present, and "Key" is a single uppercase character.
 *
 * @param configString A configuration string, following the above format.
 * @return The newly created command object.
 */
function MozileCommand() {

	
	/**** DEFINITIONS ****/
	// Define the properties for this object.

	/** Mozile Command - Type -
	 * Denotes this object as a command.
	 */
	this.type = "MozileCommand";

	/** Mozile Command - Id -
	 * The unique id by which this command is known. The usual format is "Mozile-ModuleName-CommandName".
	 */
	this.id = null;

	/** Mozile Command - Label -
	 * A word or two describing this command.
	 */
	this.label = null;

	/** Mozile Command - Tooltip -
	 * A short phrase describing this command.
	 */
	this.tooltip = null;

	/** Mozile Command - Image -
	 * The path to the image associated with this command, if any.
	 */
	this.image = null;

	/** Mozile Command - Access Key -
	 * The keyboard access key for the menuitem, or "null" for none.
	 */
	this.accesskey = null;

	/** Mozile Command - Accelerator -
	 * The keyboard shortcut for this command, or "null" for none.
	 */
	this.accelerator = null;
	
	/** Mozile Command - Namespace -
	 * The XUL namespace in which elements will be created.
	 */
	this.namespace = XULNS;
	
	/** Mozile Command - Debug Level -
	 * An integer indicating how verbose debugging should be. 0 means only critical errors are shown. Higher values mean more verbose debugging: 1="very important", 2="important", 3="normal", 4="not important"
	 */
	this.debugLevel = 0;

	/** Mozile Command - Button -
	 * The button element this command, or "null" for none.
	 */
	this.button = null;

	/** Mozile Command - Menuitem -
	 * The menuitem for this command, or "null" for none.
	 */
	this.menuitem = null;
	

	if(arguments.length > 0){
		var configArray = arguments[0];
		this.init(configArray);
	}
	else return true;

	return "Success";
	
}




/** Mozile Command - Init  -
 * Initializes the Mozile Command object, setting various properties based on the configArray.
 *
 * @param configArray An array of configuration opions, as created by Mozile.parseConfig().
 * @return True if successful, an error message otherwise.
 */
MozileCommand.prototype.init = function(configArray) {
	
	/**** VALIDATION ****/
	// Check to make sure that values are valid, then set them.
	
	this.type = configArray['name'];
	
	// Check to see if a valid id was provided.
	if(configArray['id'] && typeof(configArray['id'])!="null") {
		this.id = configArray['id'];
	}
	else {
		return "Error initializing MozileCommandList object -- invalid id provided: "+configArray['id'];
	}
	
	// Check to see if a valid label was provided.
	if(configArray['label'] && typeof(configArray['label'])!="null") {
		this.label = configArray['label'];
	}
	else {
		return "Error initializing MozileCommandList object -- invalid label provided: "+configArray['label'];
	}
	
	// Check to see if a valid tooltip was provided.
	if(configArray['tooltip']) {
		this.tooltip = configArray['tooltip'];
	}
	
	// Check to see if a valid image was provided.
	if(configArray['image']) {
		this.image = configArray['image'];
	}
	
	// Check to see if a valid accesskey was provided.
	if(configArray['accesskey']) {
		this.accesskey = configArray['accesskey'];
	}
	
	// Check to see if a valid accelerator was provided.
	if(configArray['accelerator']) {
		this.accelerator = configArray['accelerator'];
	}
	
	// Check to see if debug is an integer
	if(configArray['debugLevel']) {
		if(typeof(configArray['debugLevel'])=="number") {
			this.debugLevel = configArray['debugLevel'];
		}
		else {
			this.debugLevel = mozile.debugLevel;
		}
	}
	
	return true;
		
}


// Add the mozileDebug function as a method of the MozileCommand object.
MozileCommand.prototype.debug = mozileDebug;





/** Mozile Command - Create Button -
 * Creates an XUL button element for this command.
 * 
 * @return Button element.
 */
MozileCommand.prototype.createButton = function() {
	var f = new Array("core/MozileCore.js","MozileCommand.createButton()");
	this.debug(f,3,"Creating button");

	var button = document.createElementNS(XULNS, "toolbarbutton");
	button.setAttribute("id", this.id+"-Button");
	button.setAttribute("class", "mozileButton");
	button.setAttribute("image", this.image);
	button.setAttribute("label", this.label);
	if(this.tooltip) button.setAttribute("tooltiptext", this.tooltip);
	
	this.button = button;
	return button;
}




/** Mozile Command - Create Menuitem -
 * Creates an XUL menuitem element for this command.
 * 
 * @return The new menuitem element.
 */
MozileCommand.prototype.createMenuitem = function() {
	var f = new Array("core/MozileCore.js","MozileCommand.createMenuitem()");
	this.debug(f,3,"Creating menuitem");
	
	var menuitem = document.createElementNS(XULNS, "menuitem");
	menuitem.setAttribute("id", this.id+"-Menuitem");
	menuitem.setAttribute("class", "mozileMenuitem");
	//menuitem.setAttribute("type", "checkbox");
	menuitem.setAttribute("label", this.label);
	if(this.tooltip) menuitem.setAttribute("tooltiptext", this.tooltip);
	if(this.accesskey) menuitem.setAttribute("accesskey", this.accesskey);
	
	this.menuitem = menuitem;
	return menuitem;

}


/** Mozile Command - Is Active -
 * Checks to see if the command is active in the current selection. In the general case, isActive always returns false. But other Mozile command objects define it in non-trivial ways.
 * 
 * @return Always false.
 */
MozileCommand.prototype.isActive = function() {
	var f = new Array("core/MozileCore.js","MozileCommand.isActive()");
	this.debug(f,4,"Checking to see if this command is active");
	
	return false;
}

/** Mozile Command - Update Button -
 * Updates the XUL button and menu item for this command, setting its "active" or "checked" attribute to true if this.isActive() returns true, and to "false" otherwise.
 * 
 * @return True if the button and menuitem are active, and false otherwise 
 */
MozileCommand.prototype.update = function() {
	var f = new Array("core/MozileCore.js","MozileCommand.update()");
	this.debug(f,4,"Updating button and menuitem");
	
	var button = this.button;
	var menuitem = this.menuitem;
	
	// If isActive returns true, set the attributes to true.
	if(this.isActive()) {
		if(button) {
			button.setAttribute("active",true);
		}
		if(menuitem) {
			menuitem.setAttribute("checked",true);
		}
		return true;
	}
	// If isActive returns false, set the attributes to false.
	else {
		if(button) {
			button.setAttribute("active",false);
		}
		if(menuitem) {
			menuitem.setAttribute("checked",false);
		}
		return false;
	}

}




/** Mozile Command - Command -
 * Implements the editing command for this object. For the general Mozile command object, the command merely display an alert, but commands derived from this will implement their own code.
 * 
 * @param event The event which triggered this command.
 * @return True if successful.
 */
MozileCommand.prototype.command = function(event) {
	var f = new Array("core/MozileCore.js","MozileCommand.command()");
	this.debug(f,3,"Executing command "+event);

	alert("Command! "+ this.id +" "+ event);
	return true;

}






/** Mozile Command List Object -
 * Creates a Mozile command list object, inheriting from MozileCommand, which controls the box and menu associated with a group of Mozile editing commands.
 * @constructor
 *
 * @param configArray An array of configuration information and options, created from a congiuration string by the Mozile.parseConfig() method.
 * @return A string indicating success or an error.
 */
MozileCommandList.prototype = new MozileCommand();
MozileCommandList.prototype.constructor = MozileCommandList;
MozileCommandList.superclass = MozileCommand.prototype;

function MozileCommandList() {

	if(arguments.length > 0){
		var configArray = arguments[0];
		this.init(configArray);
	}
	else return true;


	/** Mozile Command List - Button -
	 * The box element for this command, or "null" for none. The box contains the buttons for the commands in this list.
	 */
	this.box = null;
	
	/** Mozile Command List - Menu -
	 * The menu element for this command, or "null" for none.
	 */
	this.menu = null;
	
	/** Mozile Command List - Command List -
	 * An array of all the command (list) objects in this list. Key=id, value=command object.
	 */
	this.commandList = new Array();
	
	/** Mozile Command List - Button List -
	 * An array of all the command (list) objects which use buttons in this list, in the order in which they appear.
	 */
	this.buttonList = new Array();
	
	/** Mozile Command List - Menu List -
	 * An array of all the command (list) objects which use menus or menuitems in this list, in the order in which they appear.
	 */
	this.menuList = new Array();

	return true;
}



/** Mozile Command List - Create Command -
 * Creates a new command object from a configuration string, registers it with the "mozile" object, and attaches it to this MozileCommandList.
 * 
 * Configuration String format: "MozileCommandObject: id=CommandId, label='Command Label', tooltip='Command Tool Tip', image='/path/to/image.file', accesskey=C, accelerator='Meta-C', buttonPosition=0, menuPosition=0"
 * The type=MozielCommandObject
 * Executes string "command = new MozileCommandObject(optionsArray)"
 *
 * @param configString A configuration string which describes the command to be created.
 * @return The new command object.
 */
MozileCommandList.prototype.createCommand = function(configString) {
	var f = new Array("core/MozileCore.js","MozileCommandList.createCommand()");
	this.debug(f,3,"Creating command "+ configString);

	// Parse the configString.
	var configArray = mozile.parseConfig(configString);
	
	var name = configArray['name'];
	var command;
	
	// Define the command creation string. I.e. "command = new MozileCommand(configArray)"
	var create = "command = new "+name+"(configArray)";
	
	// Evaluate the command creation string.
	eval(create);
	
	// If a buttonPosition has been specified, set the buttonPosition variable.
	var buttonPosition = null;
	if(configArray['buttonPosition'] != null) buttonPosition = configArray['buttonPosition'];
	
	// If a menuPosition has been specified, set the menuPosition variable.
	var menuPosition = null;
	if(configArray['menuPosition'] != null) menuPosition = configArray['menuPosition'];
	
	// Register this command.
	this.registerCommand(command, buttonPosition, menuPosition);
	
	return command;

}




/** Mozile Command List - Register Command -
 * Registers a command with this command list object and the "mozile" object.
 * 
 * @param commadn The command object to be registered.
 * @param buttonPosition An integer indicating the index in the buttonList where this command's button should be added. If none is given, it is added to the end of the list.
 * @param menuPosition An integer indicating the index in the menuList where this command's menuitem should be added. If none is given, it is added to the end of the list.
 * @return Always true.
 */
MozileCommandList.prototype.registerCommand = function(command, buttonPosition, menuPosition) {
	var f = new Array("core/MozileCore.js","MozileCommandList.registerCommand()");
	this.debug(f,3,"Registering command "+ command +" with id "+ command.id +" at "+ buttonPosition +" and "+ menuPosition);

	// Register the command with the global mozile object instance.
	var id = command.id
	if(command.type!="MozileCommandList") {
		mozile.registerCommand(command);
	}
	
	// Add the command to the local commandList.
	this.commandList[id] = command;
	
	// If no button position is given, add this command to the end of the buttonList.
	if(buttonPosition == null) {
		this.buttonList.push(command);
	}
	else {
		// if there is no other button at that index, insert this one.
		if(this.buttonList[buttonPosition]==null) {
			this.buttonList[buttonPosition] = command;
		}
		// if there is already a button there, splice this one in before the old one.
		else {
			this.buttonList.splice(buttonPosition, 0, command);
		}
	}
	
	// If a menu position is given, add this command to the menuList.
	if(menuPosition == null) {
		this.menuList.push(command);
	}
	else {
		// if there is no other menu item at that index, insert this one.
		if(this.menuList[menuPosition]==null) {
			this.menuList[menuPosition] = command;
		}
		// if there is already a menu item there, splice this one in before the old one.
		else {
			this.menuList.splice(menuPosition, 0, command);
		}
	}
	
	return true;
}



/** Mozile Command List - Unregister Command -
 * Removes a command from the local and global commandLists, as well as the buttonList and menuList.
 * 
 * @param id The id of the command to be removed.
 * @return Always true.
 */
MozileCommandList.prototype.unregisterCommand = function(id) {
	var f = new Array("core/MozileCore.js","MozileCommandList.unregisterCommand()");
	this.debug(f,3,"Unregistering command "+ id);

	// Unregister globally.
	mozile.unregisterCommand(command);
	
	// Unregister locally.
	this.commandList[id]=null;
	
	// Remove from buttonList.
	var i;
	for(i=0; i < this.buttonList; i++) {
		if(this.buttonList[i].id == id) {
			this.buttonList.splice(i,0);
			i--;
		}
	}
	
	// Remove from menuList.
	for(i=0; i < this.menuList; i++) {
		if(this.menuList[i].id == id) {
			this.menuList.splice(i,0);
			i--;
		}
	}

	return true;
}




/** Mozile Command List - Create Box -
 * Creates an XUL box to hold the buttons fro this command list.
 * 
 * @return The XUL box element.
 */
MozileCommandList.prototype.createBox = function() {
	var f = new Array("core/MozileCore.js","MozileCommandList.createBox()");
	this.debug(f,3,"Creating box");

	// Create the box element
	var box = document.createElementNS(XULNS, "box");
	box.setAttribute("id", this.id+"-Box");
	box.setAttribute("class", "mozileBox");
	//box.setAttribute("image", this.image);
	box.setAttribute("label", this.label);
	if(this.tooltip) box.setAttribute("tooltiptext", this.tooltip);

	// Add all the buttons from the buttonList, making sure that they exist first.
	var button;
	for(var i=0; i < this.buttonList.length; i++) {
		if(this.buttonList[i]) {
			button = this.buttonList[i].button;
			if(!button || button=="") {
				this.buttonList[i].createButton();
				button = this.buttonList[i].button;
			}
			box.appendChild(button);
		}
	}
	
	this.box = box;
	return box;
}



/** Mozile Command List - Create Button -
 * Creates an XUL button for this command list.
 * 
 * @return The XUL button element.
 */
MozileCommandList.prototype.createButton = function() {
	var f = new Array("core/MozileCore.js","MozileCommandList.createButton()");
	this.debug(f,3,"Creating button");

	// Create the button element.
	var button = document.createElementNS(XULNS, "toolbarbutton");
	button.setAttribute("id", this.id+"-Button");
	button.setAttribute("class", "mozileButton");
	button.setAttribute("image", this.image);
	button.setAttribute("label", this.label);
	if(this.tooltip) button.setAttribute("tooltiptext", this.tooltip);
	button.setAttribute("type", "menu");
	
	// Create the menupopup child element.
	var menuPopup = document.createElementNS(XULNS, "menupopup");
	button.appendChild(menuPopup);

	// Add every menuitem on the menuList to the menupopup, making sure they exist first.
	var menuitem,menu;
	for(var i=0; i < this.menuList.length; i++) {
		if(this.menuList[i]) {
			if(this.menuList[i].type!="MozileCommandList") {
				menuitem = this.menuList[i].menuitem;
				if(!menuitem || menuitem=="") {
					this.menuList[i].createMenuitem();
					menuitem = this.menuList[i].menuitem;
				}
				menuPopup.appendChild(menuitem);
			}
			// if it's a command list
			else {
				menu = this.menuList[i].menu;
				if(!menu || menu=="") {
					this.menuList[i].createMenu();
					menu = this.menuList[i].menu;
				}
				menuPopup.appendChild(menu);
			}
		}
	}
	
	this.button = button;
	return button;
}




/** Mozile Command List - Create Menu -
 * Creates an XUL menu element for this command.
 * 
 * @return The XUL menu element.
 */
MozileCommandList.prototype.createMenu = function() {
	var f = new Array("core/MozileCore.js","MozileCommandList.createMenu()");
	this.debug(f,3,"Creating menu");
	
	// Create the menu element
	var menu = document.createElementNS(XULNS, "menu");
	menu.setAttribute("id", this.id+"-Menu");
	menu.setAttribute("class", "mozileMenu");
	menu.setAttribute("label", this.label);
	if(this.tooltip) menu.setAttribute("tooltiptext", this.tooltip);
	if(this.accesskey) menu.setAttribute("accesskey", this.accesskey);
	
	// Create the menupopup child element.
	var menuPopup = document.createElementNS(XULNS, "menupopup");
	menu.appendChild(menuPopup);

	// For each menuitem on the menuList, add it to the menupopup (making sure it exists first).
	var menuitem;
	for(var i=0; i < this.menuList.length; i++) {
		if(this.menuList[i]) {
			menuitem = this.menuList[i].menuitem;
			if(!menuitem || menuitem=="") {
				this.menuList[i].createMenuitem();
				menuitem = this.menuList[i].menuitem;
			}
			menuPopup.appendChild(menuitem);
		}
	}
	
	this.menu = menu;
	return menu;

}





/** Mozile Command List - Update Box -
 * Updates the state of all the buttons for this command.
 * TODO: Not currently used, so it might not be useful at all.
 * 
 * @return Always true.
 */
MozileCommandList.prototype.updateBox = function() {
	var f = new Array("core/MozileCore.js","MozileCommandList.updateBox()");
	this.debug(f,3,"Updating box");

	return true;

}




/** Mozile Command List - Update Menu -
 * Updates the state of the menu for this command.
 * TODO: Not currently used, so it might not be useful at all.
 * 
 * @return Alwasy true.
 */
MozileCommandList.prototype.updateMenu = function() {
	var f = new Array("core/MozileCore.js","MozileCommandList.updateMenu()");
	this.debug(f,3,"Updating menu");

	return true;

}









/** Node - Insert After -
 * A simple convenience function which inserted the newNode after the referenceNode
 * 
 * @return True if the node counts as a block, false otherwise.
 */
Node.prototype.insertAfter = function(newNode, refNode) {
	if(refNode.nextSibling) {
		return this.insertBefore(newNode, refNode.nextSibling);
	}
	else {
		return this.appendChild(newNode);
	}
}




/** Node - Is Block -
 * Checks to see if this node has a CSS display property which matches the matchDisplayBlock regular expression. Currently, "block", "list-item"m and "table-cell" qualify as blocks.
 * 
 * @return True if the node counts as a block, false otherwise.
 */
Node.prototype.isBlock = function() {
	var matchDisplayBlock = /(block|list\-item|table\-cell)/;
	if(this.nodeType == 1) {		
		var display = document.defaultView.getComputedStyle(this, '').getPropertyValue("display").toLowerCase();	
		if(matchDisplayBlock.test(display)) {
			return true;
		}
	}
	return false;
}




/** Node - Get Parent Block -
 * Climbs the DOM tree until it finds an ancestor node for which isBlock is true, or it reaches the documentElement.
 * 
 * @return First node which is an ancestor and which isBlock=true, or the documentElement.
 */
Node.prototype.getParentBlock = function() {
	var thisNode = this;
	while(thisNode) {
		if(thisNode.isBlock()) {
			return thisNode;
		}
		thisNode = thisNode.parentNode;
	}
	return document.documentElement;
}




/** Node - Parent Block -
 * Gets the first ancestor block node of this node.
 * 
 * @return First node which is an ancestor and which isBlock=true, or the documentElement.
 */
Node.prototype.__defineGetter__(
	'parentBlock',
	function() {
		return this.getParentBlock();
	}
);




/** Node - Is Ancestor -
 * Climbs the DOM tree until it finds the given node, or the documentElement. 
 * 
 * @param node The node which might be an ancestor of this node.
 * @return True if the given node is an ancestor of this node, false otherwise.
 */
Node.prototype.isAncestorOf = function(node) {
	var thisNode = node;
	while(thisNode) {
		if(thisNode == this) {
			return true;
		}
		thisNode = thisNode.parentNode;
	}
	return false;
}








/** Mozile - Test Function -
 * This function is only used for testing. It gets overridden in the TestModules.
 *
 * @return Always 0.
 */
// 
Mozile.prototype.testFunction = function() {
	return 0;
}

// When everything is defined, call the mozileConfiguration function defined in mozile.js
// This will start loading the modules into the document
try {	
	mozileConfiguration();
}
catch(e) {
	alert("Error in MozileCore.js when calling mozileConfiguration: "+e);
}




/**** UTILITY FUNCTIONS ****/

/** Dump Array -
 * Displays the contents of the array in key=>value pairs, using an alert. 
 * 
 * @param arr The array to be dumped
 * @return Nothing.
 */
function dumpArray(arr) {
	var s = "Array Dump: ";
	for(key in arr) {
		s = s + key +"=>"+ arr[key] +"\n";
	}
	alert(s);
}

/** Print XML -
 * Serializes the given XML and returns it as a tring
 * 
 * @param XML The XML to be serialized.
 * @return String version of the XML.
 */
function printXML(XML) {
	if(!XML || XML=="") return "Nothing to print!!"; 
	var objXMLSerializer = new XMLSerializer;
	var output = objXMLSerializer.serializeToString(XML);
		// fix stupid uppercase tags!
	output = output.replace( /<(\/?)([A-Z]+)/g,
		function (output,p1,p2,offset,s) {
			return '<'+p1+p2.toLowerCase() ;
		} ) ;
	return output;
}


Documentation generated by JSDoc on Wed Jun 29 22:15:33 2005