core.js

Summary

This file defines the Mozile object, which contains most of Mozile's methods and properties. It defines the core of the Mozile editing code and methods for loading scripts, links, and modules. Mozile can be used as a basic editor with only this file. Additional tools and interface elements are defined in "core/interface.js".

Version: 0.7.0-preview2

Author: James A. Overton


Class Summary
Mozile

/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * 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 James A. Overton's code (james@overton.ca).
 *
 * The Initial Developer of the Original Code is James A. Overton.
 * Portions created by the Initial Developer are Copyright (C) 2005
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */




/** Mozile Core  
 * @fileoverview This file defines the Mozile object, which contains most of Mozile's methods and properties. It defines the core of the Mozile editing code and methods for loading scripts, links, and modules. Mozile can be used as a basic editor with only this file. Additional tools and interface elements are defined in "core/interface.js".
 * 
 * @link http://mozile.mozdev.org 
 * @author James A. Overton <james@overton.ca>
 * @version 0.7.0-preview2
 */







/**** 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";

// Regular expressions that match different kinds of white space. Used in the seekIP and seekText methods.
// Note: compiling these with regexp.compile() seems to (measurably) slow them down.
var matchNonWS = /\S/;
var matchTrailingWS = /(\s*)$/;
var matchLeadingWS = /^(\s*)/;

var matchMozileCore = /\/>.+?id=\"Mozile-Core-core\.js\".*?>/;

// Regular expression to match the CSS "display" properties which count as "blocks". Currently, "block", "list-item", "table-cell", and "moz-box" qualify as blocks, but this could change.
var matchDisplayBlock = /(block|list\-item|table\-cell|\-moz\-box)/;




/**** 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.
 * <p>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, toolbarPosition=fixed, toolbarUpdateFrequency=2, warnBeforeUnload=true, debugLevel=0". See the notes for "Mozile.parseConfig" for proper configuration string format.
 * @constructor
 *
 * @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-preview2";

	/** Mozile - Extension -
	 * This will be set to true by the Mozile Extension if the user has used the Mozile Extension to enhance editing on this page.
	 */
	this.extension = false;

	/** Mozile - Root -
	 * The root directory for this Mozile installation. This file "core.js" should be found at "[root]core/core.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 less verbose debugging: 4="critical", 3="very important", 2="important", 1="normal", 0="not important". Only messages with the specified level or higher will be logged.
	 */
	this.debugLevel = 4;
	
	/** Mozile - Script List -
	 * An array containing the id's of all the scripts which have been loaded using this.loadScript().
	 * TODO: Since the interface.js file might not be loaded, putting it in the list here might be misleading. Fix?
	 */
	this.scriptList = new Array("Mozile-Core-core.js", "Mozile-Core-interface.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 configuration arrays. The keys for this array are the module names, and the values are the module configuration arrays. 
	 */
	this.moduleList = new Array();
	
	/** Mozile - Command List -
	 * An array containing all of the commands (but not 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 - Save List -
	 * An array containing entries for every save method available, the default method, and the user's custom method. This is a key-value array, where the key is the method name and the value is an array. All the value-arrays include the keys: value, label, function.
	 */
	this.saveList = new Array();
	
	// Define the built in dialog save method.
	this.saveList["Dialog"] = new Array();
	this.saveList["Dialog"]["value"] = "Dialog";
	this.saveList["Dialog"]["label"] = "Save to Dialog";
	this.saveList["Dialog"]["function"] = "mozile.saveToDialog";
	this.saveList["default"] = this.saveList["Dialog"];
	
	/** Mozile - Save Config -
	 * The array of save options which is actually used to save the document. It is referred to by the saving functions, and set by the Mozile.save() method.
	 */
	this.saveConfig = null;
	
	/** Mozile - Current Editor -
	 * When an editor takes the focus, currentEditor is set 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 Position -
	 * Determines how the toolbar is positioned using CSS. Options are "absolute" and "fixed" (the default).
	 */
	this.toolbarPosition = "fixed";
	
	/** Mozile - About Interface -
	 * Mozile's about interface box element. This interface describes Mozile.
	 */
	this.aboutInterface = null;
	
	/** Mozile - Save Interface -
	 * Mozile's save interface box element. This is a "Save As" interface.
	 */
	this.saveInterface = null;
	
	/** Mozile - Source Interface -
	 * Mozile's source interface box element. This interface displays the source code for the document when the "Display" save method is used.
	 */
	this.sourceInterface = null;
	
	/** Mozile - Message Interface -
	 * Mozile's message interface box element. This interface shows the status and debugging messages that Mozile has collected.
	 */
	this.messageInterface = null;
	
	/** Mozile - Statusbar -
	 * Mozile's status bar element.
	 */
	this.statusbar = null;
	
	/** Mozile - Toolbar Update -
	 * An integer specifying how often the toolbar should update. 0=Never, 1=Rarely, 2=Often.
	 */
	this.toolbarUpdateFrequency = 2;
	
	/** Mozile - First Toolbar Show -
	 *  A flag which is "true" if the Mozile toolbar has never been shown (i.e. Mozile.showToolbar() has never been called), and false otherwise.
	 */
	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 information about the last insertion point. The value is an array with entries: node, offset, keyCode.
	 */
	this.lastIP = new Array("","","");

	/** Mozile - Key Counter -
	 * Counts the number of keypresses which lead to character insertion. Used to trigger storeState undo/redo steps.
	 */
	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 output methods (documentToXML, etc.) 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 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 a toolbarPosition setting was provided.
	if(optionsArray["toolbarPosition"]) {
		this.toolbarPosition = optionsArray["toolbarPosition"];
	}

	// 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 the onbeforeunload event handler. If warnBeforUnload has been set to "false", then the function returns nothing. Otherwise (the default case) a message is returned.
	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 -
 * A basic debugging tool. It logs messages to the mozileDebugArray, but only if their level exceeds the Mozile.debugLevel setting, or they are marked "Status Message".
 * 
 * @param details An array of information. The first two fields are "File", and "Function" (usually a function name). Fancier debugging functions might use more fields. 
 * @param level An integer describing the importance of the debugging message. 4="critical", 3="very important", 2="important", 1="normal", 0="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) return true;
	// If the level is higher than the debug level or the "Status Message" value is true, then save the message.
	if(level >= this.debugLevel || details["Status Message"]) {
		var date = new Date();
		// add it to the debugging array
		mozileDebugList.push([date.toLocaleString(), details, level, message]);
	}
	
	return true;
}

// Create the global debug list for this document.
var mozileDebugList = new Array();

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





/** Mozile - Status -
 * Sets the content of the Mozile statusbar and enters a special kind of debugging message. In addition to the normal debug arguments, status messages can include a "value" argument which indicates the percentage displayed on the progress bar (if none is given or the value is "false" then the progress bar is hidden). They can also include a "more" string, which will be entered into the "oncommand" attribute of the mozileMoreButton; this is usually used to popup a window or dialog with additional information.
 * 
 * @param details An array of information. The first two fields are "File", and "Function" (the a function name). Fancier debugging functions might use more fields. 
 * @param level An integer describing the importance of the debugging message. 4="critical", 3="very important", 2="important", 1="normal", 0="not important".
 * @param message A string containing the debugging message.
 * @param value Optional An integer between 0 and 100 indicating the value of the progressmeter. If no value is provided or the value is "false", the progressmeter is not shown.
 * @param more Optional A string which is placed in the "oncommand" attribute of the "mozileMoreButton". If no string is provided, the button is not shown.
 * @return Always true.
 */
Mozile.prototype.status = function(details, level, message) {
	var f = new Array();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.status()";
	//this.debug(f,1,"Setting status");

	try {
		var value = false;
		if(arguments.length > 3) {
			value = arguments[3];
		}
		var more = false;
		if(arguments.length > 4) {
			more = arguments[4];
		}
		//this.debug(f,1,"Setting status to: "+ message +" -> "+ value +"% -> "+ more);
	
		// Send this message to the debug function.
		var g = new Array();
		for(key in details) {
			g[key] = details[key];
		}
		g["Status Message"]=true;
		var msg = message;
		if(value) msg = msg +" "+ value +"%";
		this.debug(g, level, msg);


		// If the statusbar doesn't exist, just return now.
		if(this.statusbar == null) return true;

		// Otherwise the status bar does exist, so try to set its value.
		this.statusbar.firstChild.value = message;
		var progressmeter = this.statusbar.childNodes[1];
		var moreButton = this.statusbar.childNodes[2];

		// If "value" is set, show the progressmeter and set its value.	
		if(value != false) {
			progressmeter.value = value;
			progressmeter.collapsed=false;
		}
		// Otherwise hide the progresmeter and zero it.
		else {
			progressmeter.value = 0;
			progressmeter.collapsed=true;
		}

		// If "more" is set, show the moreButton and set the oncommand attribute.	
		if(more != false) {
			moreButton.setAttribute("oncommand", more);
			moreButton.collapsed = false;
		}
		// Otherwise hide it and clear the oncommand attribute.
		else {
			moreButton.setAttribute("oncommand", "");	
			moreButton.collapsed = true;
		}
	}
	catch(e) {
		this.debug(f,2,"Failed to set status message: "+ details["File"] +", "+ details["function"] +", "+ message);
	}

	return true;
}







/** Mozile - Show Caret -
 * Activates the editing caret. This will only work if it is run from the privileged environment of the Mozile Extension, and if not privileged it does nothing.
 * 
 * @return True if the caret is now on, false otherwise.
 */
Mozile.prototype.showCaret = function() {
	if(mozile.extension) {
		netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
		var mozilePrefs = Components.classes['@mozilla.org/preferences-service;1'].getService(Components.interfaces.nsIPrefService).getBranch(null);	
		mozilePrefs.setBoolPref('accessibility.browsewithcaret', true);
		return true;
	}
	
	return false;

}




/** Mozile - Hide Caret -
 * Deactivates the editing caret. This will only work if it is run from the privileged environment of the Mozile Extension, and if not privileged it does nothing. If the user has set her mozile.caretAlwaysOn preference to "true", then the caret is left on.
 * 
 * @return True if the caret is now on, false otherwise.
 */
Mozile.prototype.hideCaret = function() {
	if(mozile.extension) {
		netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
		var mozilePrefs = Components.classes['@mozilla.org/preferences-service;1'].getService(Components.interfaces.nsIPrefService).getBranch(null);
		if(!mozilePrefs.getBoolPref("mozile.caretAlwaysOn")) {
			mozilePrefs.setBoolPref('accessibility.browsewithcaret', false);
		}
		return true;
	}
	
	return false;
}








/** Mozile Handle Focus -
 * Takes appropriate action when a Mozile editor gains focus: shows the caret and toolbar, then sets the currentEditor.
 * 
 * @param event The focus event to be handled. 
 * @return True if successful.
 */
function mozileHandleFocus(event) {
	var f = new Array();
	f["File"] = "core/core.js";
	f["Function"] = "mozileHandleFocus()";
	mozile.debug(f,1,"Mozile gains focus "+ event);
	
	mozile.showCaret();
	
	//if(mozileInterface && mozile.toolbar == null) mozile.createToolbar();
	
	if(mozile.toolbar != null) {
		mozile.showToolbar(); 
		mozile.moveToolbar();
	}
	
	// TOOD: This might not work right with Mozile.makeDocumentEditable()
	mozile.setCurrentEditor(event.originalTarget);
	
	return true;
}



/** Mozile Handle Blur -
 * Takes appropriate action when a Mozile editor loses focus: hide the caret and toobar.
 * 
 * @param event The blur event to be handled. 
 * @return True if successful.
 */
function mozileHandleBlur(event) {
	var f = new Array();
	f["File"] = "core/core.js";
	f["Function"] = "mozileHandleFocus()";
	mozile.debug(f,1,"Mozile loses focus "+ event);
	
	mozile.hideCaret();
	
	if(mozile.toolbar != null) mozile.hideToolbar();
	
	return true;
}




/** Mozile Handle Keypress -
 * Decides what action to take for the given "keypress" event, and calls the appropriate function. Somewhat complicated. I've tried to optimize it for speed, because it gets called frequently, so it only asks for information as needed.
 * 
 * @param event The keypress event to be handled. 
 * @return True when successful, false otherwise.
 */
function mozileHandleKeypress(event) {
	var f = new Array();
	f["File"] = "core/core.js";
	f["Function"] = "mozileHandleKeypress()";
	//mozile.debug(f,1,"Handling keypress event "+ event);
	
	// Ignore the function keys (i.e. F7).
	if(event.keyCode >= 112 && event.keyCode <= 135) {
		// mozile.debug(f,0,"Function key keypress");
		return true;
	}
	
	var selection = window.getSelection();

	// Make sure we have a focusNode
	if(!selection.focusNode) {
		// mozile.debug(f,0,"No selection");
		return true;
	}
	else {
		// mozile.debug(f,0,"Selection "+ selection);
	}


	// Handle arrow keys. When the cursor has moved, update the toolbar. Also, don't let the cursor get stuck against an empty element.
	if(event.keyCode >=33 && event.keyCode <= 40) {	
		// Handle left and right arrow keys. The cursor can get stuck against an empty node when moving left or right. When this happens, we can help it get unstuck.
		if(event.keyCode == 37 || event.keyCode == 39) {
			// Check to see if the cursor hasn't moved since the last keypress
			if(selection.focusNode == mozile.lastIP[0] && selection.focusOffset == mozile.lastIP[1] && event.keyCode == mozile.lastIP[2] ) {
				// Check to see if we are at the beginning of end of the node, and set the proper direction
				var direction = null;
				if(event.keyCode==37 && selection.focusOffset == 0) { 
					direction="previous";
				}
				if(event.keyCode==39 && selection.focusOffset == selection.focusNode.textContent.length) { 
					direction="next";
				}
				
				// If the direction was set, then find a new text node in that direction.
				if(direction) {
					// mozile.debug(f,1,"Cursor seems stuck. Jumping to "+direction+" IP.");		
					// Seek a new IP
					var node = mozile.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);
					
					// mozile.debug(f,0,"Arrow keypress, direction "+ direction);
					event.stopPropagation();
					event.preventDefault();
				}

				// If the direction wasn't set, we just store the IP and don't worry about unsticking the cursor.
				else {
					// Store the current IP
					mozile.lastIP = [selection.focusNode, selection.focusOffset, event.keyCode];
				}
			}
			else {
				// Store the current IP
				mozile.lastIP = [selection.focusNode, selection.focusOffset, event.keyCode];
				//alert(mozile.lastIP);
			}
			return true;
		}
	
		// If the toolbar is supposed to update often, update it now.
		if(mozile.toolbar != null && mozile.toolbarUpdateFrequency==2) {
			mozile.updateToolbar();
		}
		return true;
	}


	// Handle Modifiers. Check for keyboard shortcuts triggering commands.
	if((event.ctrlKey || event.metaKey) && mozile.keyboardShortcuts) {
		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();
		// mozile.debug(f,0,"Accelerator keypress: "+ accel);
		//alert(accel);
		//dumpArray(mozile.acceleratorList);
		if(mozile.acceleratorList[accel]) {
			mozile.executeCommand(mozile.acceleratorList[accel].id, event);
			if(mozile.toolbar != null) mozile.updateToolbar();
			// mozile.debug(f,0,"Accelerator command found: "+ mozile.acceleratorList[accel].id);
			event.stopPropagation();
  		event.preventDefault();
			return true;
		}
		else {
			// mozile.debug(f,0,"Accelerator command not found");
			return true;
		}
	}


	// Check editable. If this isn't the same node as the last time we checked, check that the parent node is modifiable and accepts input.
	if(selection.focusNode != mozile.lastIP[0]) {
		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") {
				mozile.debug(f,1,"Not user modifiable!");
				return true;
			}
			else {
				// mozile.debug(f,0,"New modifiable node.");
			}
		} catch(e) {
			alert("Bad selection? "+e+"\n"+selection.focusNode);
		}
	}
	
	// Store this IP
	mozile.lastIP = [selection.focusNode, selection.focusOffset, event.keyCode];

	
	// Handle delete and backspace keys.
	if(event.keyCode == event.DOM_VK_BACK_SPACE) {
		mozile.deletion("previous");
		// mozile.debug(f,0,"Back space keypress");
		event.stopPropagation();
  	event.preventDefault();
		return true;
	}
	if(event.keyCode == event.DOM_VK_DELETE){
		mozile.deletion("next");
		// mozile.debug(f,0,"Delete keypress");
		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 && event.keyCode != event.DOM_VK_ENTER && event.keyCode != event.DOM_VK_RETURN && event.keyCode != event.DOM_VK_TAB ) {
		mozile.insertString(String.fromCharCode(event.charCode));
		mozile.keyCounter++;
		if(mozile.keyCounter > mozile.maxKeyCount) mozile.storeState("Typing");
		// mozile.debug(f,0,"Non-command keypress "+event.charCode);
		event.stopPropagation();
  	event.preventDefault();
  	return;
	}

	
	// 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") {
			mozile.insertString("\n");
		}
		else {
			mozile.splitBlock();
		}
		mozile.storeState("Enter Key");
		// mozile.debug(f,0,"Enter keypress");
		event.stopPropagation();
		return true;
	}

	// Handle the tab key by inserting a tab. We might add other behaviours later.
	if(event.keyCode == event.DOM_VK_TAB) {
		mozile.insertString("\t");
		mozile.keyCounter++;
		if(mozile.keyCounter > mozile.maxKeyCount) mozile.storeState("Typing");
		// mozile.debug(f,0,"Tab keypress");
		event.stopPropagation();
  	event.preventDefault();
		return true;
	}

	mozile.debug(f,1,"Keypress not handled");
	return true;
}


/** Mozile Handle Keyup -
 * Decides what action to take for the given "keyup" event, and calls the appropriate functions: update the toolbar and store the current state for undo/redo.
 * 
 * @param event The keyup event to be handled. 
 * @return True when successful, false otherwise.
 */
function mozileHandleKeyup(event) {
	var f = new Array();
	f["File"] = "core/core.js";
	f["Function"] = "mozileHandleKeyup()";
	//mozile.debug(f,1,"Handling keyup event "+ event);
	
	// if an arrow key has been release, update the toolbar.
	if(event.keyCode >=33 && event.keyCode <= 40) {
		if(mozile.toolbar != null) mozile.updateToolbar();
		return true;
	}

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

	
	return true;
}






/** Mozile Handle Mouseup -
 * Takes appropriate action when Mozile detects a mouseup event: show the caret and the toolbar.
 * 
 * @param event The mouseup event to be handled. 
 * @return True if successful.
 */
function mozileHandleMouseup(event) {
	var f = new Array();
	f["File"] = "core/core.js";
	f["Function"] = "mozileHandleMouseup()";
	mozile.debug(f,1,"Mouseup "+ event);
	
	mozile.showCaret();
	
	if(mozile.toolbar != null) mozile.updateToolbar();
	
	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. The selection is then collapsed and set to the end of the inserted string.
 * 
 * @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();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.insertString()";
	this.debug(f,1,"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,4,"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 when trying to extend selection: "+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();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.insertFragment()";
	this.debug(f,1,"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,4,"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) 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();
	// f["File"] = "core/core.js";
	// f["Function"] = "Mozile.deletion()";
	// this.debug(f,1,"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, true);
		
		// Check to make sure that the new insertion point node exists.
		if(!arr || !arr[0]) return false;
		
		// If this is not the same node as the one we started with, check to make sure that the new insertion point node is marked modifiable.
		if(!arr[2]) {
			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;
		}
	}
	
	// Get some information about the range.
	var range = selection.getRangeAt(0).cloneRange();
	var startBlock = range.startContainer.parentBlock;
	var endBlock = range.endContainer.parentBlock;

	// Now delete the contents of the selection.
	// If the selection is collapsed, or starts and ends in the same block, then delete its contents.
	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();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.splitBlock()";
	this.debug(f,1,"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.	Otherwise do nothing.
	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);
		
		// Clone the target node "shallow" (not deep)
		var newNode = node.cloneNode(false);

		// Put the contents of the range into the newNode (this removes them from the old node).
		var rangeString = range.toString();
		
		if(rangeString!="") {
			newNode.appendChild(range.extractContents());
		}

		// Handle the different kinds of rangeStrings we can have.	
		var textNode = null;	
		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,1,"Major error splitting block: "+e);
			// do nothing
		}
		
	}
	
	
	this.debug(f,1,"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. 
 * @param firstNode Boolean. True if this is the first node being tested, and false otherwise. 
 * @return An array, with the first entry being a text node, the second being the offset in that node, and the third a boolean value which is true if we are still in the first node tested.
 */
Mozile.prototype.seekIP = function(direction, startNode, startOffset, firstNode) {
	// var f = new Array();
	// f["File"] = "core/core.js";
	// f["Function"] = "Mozile.seekIP()";
	// this.debug(f,1,"Seeking IP in direction "+ direction +" starting at "+startNode+" "+startOffset);
	
	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, false);
		else return false;
	}
	
	var content = startNode.textContent;


	// 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, false);
		else return false;
	}
	

	var arr = new Array();

	// Get a substring from the contents, from the startOffset to the beginning or end. Then use a regular expression to get the leading or trailing white-space.
	var substring, result;
	if(direction=="next") {
		substring = content.substring(startOffset, content.length);
		result = matchLeadingWS(substring);
	}
	else {
		substring = content.substring(0, startOffset);
		result = matchTrailingWS(substring);
	}
	
	// Hopefully there was some result...
	if(result) {
		
		// If the result has length is 0 or 1, then we ignore the special white-space rules for HTML and XML, and just send the subsequent offset.
		if(result[0].length < 2) {
			arr[0] = startNode;
			if(direction=="next") arr[1] = startOffset + 1;
			else arr[1] = startOffset - 1;
			arr[2] = firstNode;
			return arr;		
		}
		
		// Handle longer results.
		else {
			// We need to know the white-space mode.
			var wsMode = document.defaultView.getComputedStyle(startNode.parentNode, '').getPropertyValue("white-space").toLowerCase();
			
			// If the white-space mode is "pre", then again we ignore the special whie-space rules, and send the subsequent offset.
			if(wsMode == "pre") {
				arr[0] = startNode;
				if(direction=="next") arr[1] = startOffset + 1;
				else arr[1] = startOffset - 1;
				arr[2] = firstNode;
				return arr;
			}

			// If the result is long, but not as long as the substring, then move by the length of the result.
			if(result[0].length < substring.length) {
				arr[0] = startNode;
				if(direction=="next") arr[1] = startOffset + result[0].length;
				else arr[1] = startOffset - result[0].length;
				arr[2] = firstNode;
				return arr;
			}

			// If the result has length equal to the substring, we'll need to get move to another text node and find the IP there. (The length should never be greater, but we will handle that the same way.)
			else {	
				newNode = this.seekTextNode(direction, startNode);
				if(newNode) {
					if(direction=="next") return this.seekIP(direction, newNode, 1, false);
					else return this.seekIP(direction, newNode, newNode.textContent.length, false);
				}
				else {
					// No new node!
					return false;
				}
			}
		}
		
	}
	else {
		// There was some problem with the regular expression.
		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, it returns 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();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.seekTextNode()";
	this.debug(f,1,"Seeking Text Node in direction "+ direction +" starting at "+startNode);
	
	// Create a tree walker for the current editor.
	var treeWalker = document.createTreeWalker(this.currentEditor, 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();
		
	
	// Loop until a non-white-space or "pre" text node is reached
	var wsMode;
	while(checkNode) {
	
		// If the node contains non-white-space characters, then return it.
		if(matchNonWS.test(checkNode.textContent)) {
			this.debug(f,1,"Returning text node: "+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,3,"No editable text node in direction "+direction);
	return false;
}





/** Mozile - Add Style Sheet -
 * Inserts a style element in the document, and grabs the CSSStyleSheet object associated with the new element. Then sets the Mozile.styleSheet property to that style sheet. The style sheet is used to attach XBL to Mozile editors and other widgets.
 * 
 * @return True if successful.
 */
Mozile.prototype.addStyleSheet = function() {
	var f = new Array();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.addStyleSheet()";
	this.debug(f,0,"Adding style sheet.");
	
	var id = "Mozile-Core-StyleSheet";
	var styleTag, head;

	// Create the style element.
	// 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];
	} 
	// Handle the XML case.
	else {
		styleTag = document.createElementNS(XHTMLNS,"style");
	}

	// Add attributes to the element.
	styleTag.setAttribute("id", id);
	styleTag.setAttribute("type", "text/css");
	
	// Add the element to the head of the document in the X/HTML case.
	if(head) {
		head.appendChild(styleTag);
	}
	// Add the element to the beginning of the document in the XML case.
	else {
		document.documentElement.insertBefore(styleTag,document.documentElement.firstChild);
	}
	// Make sure that Mozile knows that the element has been added.
	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;
		}
	}

	// Set the Mozile styleSheet property to this style sheet.
	this.debug(f,1,"Style sheet selected: "+ i +" "+ sheet +" "+ sheet.href);
	this.styleSheet = sheet;
	
	return sheet;
}






/** Mozile - Create Editor -
 * Creates a Mozile editor in the document using its id. Two CSS rules are added to this.styleSheet, 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();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.createEditor()";
	this.debug(f,1,"Creating editor "+ id +" "+ options);
	
	// Get the styleSheet. If it doesn't exist then add it.
	if(!this.styleSheet) this.addStyleSheet();

	// Define the new rules.
	var rule1 = "#"+ id +" { -moz-binding: url(" + this.root +"core/core.xml#editor); -moz-user-focus: normal;	-moz-user-modify: read-write; }";
	var rule2 = "#"+ id +" * { -moz-user-focus: ignore; }";
	
	// Add the rules.
	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. TODO: No options are currently implemented.
 * @return True if successful.
 */
Mozile.prototype.createEditors = function(selector, options) {
	var f = new Array();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.createEditors()";
	this.debug(f,1,"Creating editor "+ selector +" "+ options);

	// Get the styleSheet. If it doesn't exist then add it.
	if(!this.styleSheet) this.addStyleSheet();
	
	// Define the new rules
	var rule1 = selector +" { -moz-binding: url(" + this.root +"core/core.xml#editor); -moz-user-focus: normal;	-moz-user-modify: read-write; }";
	var rule2 = selector +" * { -moz-user-focus: ignore; }";

	// Add the rules to the sheet.
	this.styleSheet.insertRule(rule1, this.styleSheet.cssRules.length);
	this.styleSheet.insertRule(rule2, this.styleSheet.cssRules.length);

	return true;
}





/** Mozile - Make Document Editable -
 * Makes the entire document editable. Instead of using XBL bindings on elements selected by CSS, this function creates a number of document-wide eventListeners.
 * 
 * @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.makeDocumentEditable = function(options) {
	var f = new Array();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.makeDocumentEditable()";
	this.debug(f,1,"Making document editable "+ options);
	
	// On Focus
	document.addEventListener("focus", mozileHandleFocus, false);
	
	// On Blur
	document.addEventListener("blur", mozileHandleBlur, false);
	
	// On Keypress
	document.addEventListener("keypress", mozileHandleKeypress, false);
	
	// On Keyup
	document.addEventListener("keyup", mozileHandleKeyup, false);
	
	// On Mouseup
	document.addEventListener("mouseup", mozileHandleMouseup, false);
	
	// Get the root element name
	var root;
	if(document.documentElement.nodeName.toLowerCase() == "html") {
		root = "body";
	}
	else {
		root = document.documentElement.nodeName;
	}
	
	
	// Get the styleSheet. If it doesn't exist then add it.
	if(!this.styleSheet) this.addStyleSheet();
	
	// Create the CSS rules
	var rule1 = root +" { -moz-user-focus: normal; -moz-user-modify: read-write; }";
	var rule2 = root +" * { -moz-user-focus: ignore; }";
	
	// Apply the CSS rules to the sheet.
	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 method is called by the XBL contructor code of new Mozile editors when they are created.
 * 
 * @param element The root element of the new editor.
 * @return True if successful.
 */
Mozile.prototype.registerEditor = function(element) {
	var f = new Array();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.registerEditor()";
	this.debug(f,1,"Registering editor "+ element);

	this.editorList.push(element);

	return true;
}



/** Mozile - Set Current Editor -
 * Sets the this.currentEditor 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();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.setCurrentEditor()";
	this.debug(f,1,"Setting current editor "+ element);

	this.currentEditor = element;

	return true;
}






/** Mozile - Clean Up -
 * Removes all traces of Mozile in the document. It does this by removing all of the element on the scriptList, linkList, and styleList, the toolbar element, and the "tabindex" attributes of all elements with tabindex=12345. It's meant to act on a clone of the document element, and not the document itself (which would disable Mozile in the process).
 * 
 * @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();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.cleanUp()";
	this.debug(f,1,"Cleaning up element "+element);

	var i,j=0;
	this.debug(f,1,"Scripts: "+this.scriptList);
	// 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]) {
				this.debug(f,0,"Removing script "+ this.scriptList[j]);
				scripts[i].parentNode.removeChild(scripts[i]);
			}
		}
	}
	
	// This won't remove the Mozile-Core-core.js file, unfortunately. Maybe because it's running the script? That has to be done with a regular expression.
	
	// 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");
	while(moziletoolbars.length){
		moziletoolbars[0].parentNode.removeChild(moziletoolbars[0]);
	}
	
	// Clean up the "tabindex" attributes that we've added. This is an effective but inefficient method: use a treewalker over elements, and check for a "tabindex" matching the (hopefully unique) code 12345.
	var treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_ELEMENT, null, false);
	var current = treeWalker.firstChild();
	while(current) {
		if(current.getAttribute("tabindex") && current.getAttribute("tabindex") == "12345") {
			current.removeAttribute("tabindex");
		}
		current = treeWalker.nextNode();
	}
	
	// Check the element itself for the tabindex attribute.
	if(element.getAttribute("tabindex") && element.getAttribute("tabindex") == "12345") {
		element.removeAttribute("tabindex");
	}
	
	return element;
}





/** Mozile - Document To HTML -
 * Extract the contents of the document as HTML, first cleaning up any mess that Mozile has made. It maintains any XML declaration, XML processing instructions, and Doctype declarations it finds.
 * <p>Since editing modes are not yet supported, this method behaves very much the documentToXML method.
 * 
 * @return String of the serialized HTML contents.
 */
Mozile.prototype.documentToHTML = function() {
	var f = new Array();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.documentToHTML()";
	this.debug(f,1,"Rendering document to HTML string");

	
	// Get instances of the XML tools.
	var serializer = new XMLSerializer;
	var	evaluator =	new	XPathEvaluator();
	
	var doc = mozile.getDocument();
	
	// Set the XML declaration, if the document has an xmlVersion, and make it into a string.
	var xmlDeclaration = "";
	if(doc.xmlVersion) {
		xmlDeclaration = '<?xml version="'+ doc.xmlVersion +'" encoding="'+ doc.xmlEncoding +'"?>\n'
	}
	
	// Get the DOCTYPE if there is one, and serialize it.
	var doctypeDeclaration = ""
	if(doc.doctype) {
		doctypeDeclaration = serializer.serializeToString(doc.doctype) +"\n";
	}
		
	// Get all of the processing instructions (only from the top of this document?), and create a string from them.
	var PIList = evaluator.evaluate("/processing-instruction()", doc, null, XPathResult.ANY_TYPE, null);
	var PI = PIList.iterateNext();
	var PIString = "";
	while (PI) {
		PIString += "<?"+ PI.target +" "+ PI.data + "?>\n";
		PI = PIList.iterateNext();
	}
	this.debug(f,1,"Processing Instructions: "+PIString);
	
	// Make a copy of the documentElement and clean it up.
	var newDoc = doc.documentElement.cloneNode(true)
	newDoc = this.cleanUp(newDoc);	
	
	// Serialize the contents.
	var contents = serializer.serializeToString(newDoc);
	
	// Remove the Mozile-Core-core.js script tag.
	contents = contents.replace(matchMozileCore, "/>");
	
	// Amalgamate the XML declaration, the doctype declaration, the processing instructions, and the content.
	contents = xmlDeclaration + doctypeDeclaration + PIString + contents;
	
	// Changes have been seen.
	this.changesSaved = true;
	
	return contents;
}



/** Mozile - Document To XML -
 * Extract the contents of the document as XML, first cleaning up any mess that Mozile has made. It maintains any XML declaration, XML processing instructions, and Doctype declarations it finds.
 * <p>Because Mozile's mode support is not finished, this method isn't exactly as it should be. It lowercases all element names to make them look more like XML.
 * 
 * @return String of the serialized XML contents.
 */
Mozile.prototype.documentToXML = function() {
	var f = new Array();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.documentToXML()";
	this.debug(f,1,"Rendering document to XML string");
	
	// Get instances of the XML tools.
	var serializer = new XMLSerializer;
	var	evaluator =	new	XPathEvaluator();
	
	var doc = mozile.getDocument();
	
	// Set the XML declaration, if the document has an xmlVersion, and make a properly formatted string from it.
	var xmlDeclaration = "";
	if(doc.xmlVersion) {
		xmlDeclaration = '<?xml version="'+ doc.xmlVersion +'" encoding="'+ doc.xmlEncoding +'"?>\n'
	}
	
	// Get the DOCTYPE if there is one, and serialize it.
	var doctypeDeclaration = ""
	if(doc.doctype) {
		doctypeDeclaration = serializer.serializeToString(doc.doctype) +"\n";
	}
		
	// Get all of the processing instructions (only from the top of this document?), and make a string out of them.
	var PIList = evaluator.evaluate("/processing-instruction()", doc, null, XPathResult.ANY_TYPE, null);
	var PI = PIList.iterateNext();
	var PIString = "";
	while (PI) {
		PIString += "<?"+ PI.target +" "+ PI.data + "?>\n";
		PI = PIList.iterateNext();
	}
	this.debug(f,1,"Processing Instructions: "+PIString);
	
	// Make a copy of the documentElement and clean it up.
	var newDoc = doc.documentElement.cloneNode(true)
	newDoc = this.cleanUp(newDoc);	
	
	// Serialize the contents.
	var contents = serializer.serializeToString(newDoc);
	
	// Remove the Mozile-Core-core.js script tag.
	contents = contents.replace(matchMozileCore, "/>");
	
	// Amalgamate the XML declaration, the doctype declaration, the processing instructions, and the content.
	contents = xmlDeclaration + doctypeDeclaration + PIString + contents;
	
	// Changes have been seen.
	this.changesSaved = true;
	
	// Lowercase the element names, and return the string.	
	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. A simpler version of the documentToHTML method.
 * 
 * @return String of the serialized XML contents.
 */
Mozile.prototype.editorToHTML = function() {
	var f = new Array();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.editorToHTML()";
	this.debug(f,1,"Rendering current editor "+ this.currentEditor +" to XML string");

	if(!this.currentEditor) {
		this.debug(f,4,"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);
	
	// Remove the Mozile-Core-core.js script tag.
	contents = contents.replace(matchMozileCore, "/>");
	
	// 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. A simpler version of documentToXML.
 * 
 * @return String of the serialized XML contents.
 */
Mozile.prototype.editorToXML = function() {
	var f = new Array();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.editorToXML()";
	this.debug(f,1,"Rendering current editor "+ this.currentEditor +" to XML string");

	if(!this.currentEditor) {
		this.debug(f,4,"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);
	
	// Remove the Mozile-Core-core.js script tag.
	contents = contents.replace(matchMozileCore, "/>");
	
	// Changes have been seen.
	this.changesSaved = true;

	// Lowercase the element names and return the string.	
	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 - Content -
 * Returns the string of Mozile's content, depending on the settings in the Mozile.saveConfig array.
 * 
 * @return Always true.
 */
Mozile.prototype.content = function() {
	var f = new Array();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.content()";
	this.debug(f,1,"Getting Mozile content.");
	
	//dumpArray(this.saveConfig);
	var content = "documentTo";
	var format = "XML";
	
	// If there's no saveConfig, then send a debug message and continue.
	if(!this.saveConfig) {
		this.debug(f,2,"Mozile.saveConfig not set.");
	}
	// Format a method name from the saveConfig entries.
	else {
		if(this.saveConfig["content"]) {
			switch(this.saveConfig["content"]) {
				case "document":
					content = "documentTo";
					break;
				case "current":
					content = "editorTo";
					break;
				default:
					content = "documentTo";
					break;
			}
		}
		
		if(this.saveConfig["format"]) {
			switch(this.saveConfig["format"]) {
				case "HTML":
					format = "HTML";
					break;
				case "XHTML":
					format = "XML";
					break;
				case "XML":
					format = "XML";
					break;
				default:
					format = "XML";
					break;
			}
		}
	}
	
	
	this.debug(f,1,"Saving "+ content + format);
	var result;

	// Try to evaluate the method name we build above.
	try {
		result = eval("this."+ content + format +"()");
	} catch(e) {
		this.debug(f,1,"Bad result: "+e);
	}

	return result;
}




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

	// If this is the first change after a save, then change the status message and set the changesSaved to false.
	if(this.changesSaved == true) {
		this.status(f,1,"Editing");
		this.changesSaved = false;
	}
	
	// Reset the key counter.
	this.keyCounter = 0;
	
	return true;

}


/** Mozile - Store Selection -
 * Store the selection in a recoverable form. The technique is to get the index of the target element within the list of all elements with the same name, the index of the text node within the element, and the offset within the text node. We find these for both the anchor and focus nodes of the selection, and store those values.
 * 
 * @return An array with all the details required to restore the selection: anchorElementName, anchorElementIndex, anchorNodeIndex, anchorNodeOffset, focusElementName, focusElementIndex, focusNodeIndex, focusNodeOffset.
 */
Mozile.prototype.storeSelection = function() {
	var f = new Array();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.storeSelection()";
	this.debug(f,1,"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;
			}
		}

	}
		
	// Include all of the collected information in an array.
	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. The technique is to get a list of all elements of a given anchorElementName, then select the one with the given anchorElementIndex. After that the text node with the given anchorNodeIndex is selected, and combined with the anchorNodeOffset we can find the anchor for the selection. The same goes for the focus. Then we build a new range and set the selection.
 * 
 * @return True if successful.
 */
Mozile.prototype.restoreSelection = function(selectionArray) {
	var f = new Array();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.restoreSelection()";
	this.debug(f,1,"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;

}







/** Node - Insert After -
 * A simple convenience function which inserted the newNode after the referenceNode.
 * 
 * @return The inserted node.
 */
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 (defined globally at the top of this file). Currently, "block", "list-item", "table-cell", and "moz-box" qualify as blocks, but this could change.
 * 
 * @return True if the node counts as a block, false otherwise.
 */
Node.prototype.isBlock = function() {
	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 toward the root of the DOM tree until it finds an ancestor node for which isBlock is true, or it reaches the documentElement.
 * 
 * @return Node. First node which is an ancestor and which isBlock=true, or the documentElement. In practise this will always be an element.
 */
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 - Get Selection -
 * Returns the Selection object for the current Mozile document. If this method is executed from inside the extension, it will return the Selection from the focused window. Otherwise, it will return the Selection from the current window. This is only used by code which may or may not be executed in the extension's context, like the copy and paste functions.
 * 
 * @return Selection object.
 */
Mozile.prototype.getSelection = function() {
	if(mozile.extension) {
		return document.commandDispatcher.focusedWindow.getSelection();
	}
	else {
		return window.getSelection();
	}
}

/** Mozile - Get Document -
 * Returns the Document object for the current Mozile document. If this method is executed from inside the extension, it will return the Document from the focused window. Otherwise, it will return the current Document. This is only used by code which may or may not be executed in the extension's context, like the copy and paste functions.
 * 
 * @return Document object.
 */
Mozile.prototype.getDocument = function() {
	if(mozile.extension) {
		return document.commandDispatcher.focusedWindow.document;
	}
	else {
		return document;
	}
}






/** 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();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.loadScript()";
	this.debug(f,1,"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();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.unloadScript()";
	this.debug(f,1,"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();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.loadLink()";
	this.debug(f,1,"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();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.unloadLink()";
	this.debug(f,1,"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 configString A module configuration string. The most basic case is simply "moduleName" (i.e. "CopyCutPaste"), but other options can be specified. For example: "CopyCutPaste: minVersion=1.0.2, maxVersion=1.1.2, notVersion=1.0.6, notVersion=1.1.0, requireVersion=1.0.7, remoteVersion=1.0.7, forceRemote=false, remotePath='/path/to/module/' "
 * @return False if the module is already loaded, true otherwise.
 */
Mozile.prototype.loadModule = function(configString) {
	var f = new Array();
	f["File"] = "core/core.js";
	f["Function"] = "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,4,"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'];
	configArray['path'] = src + "/";
	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";
	configArray['id'] = id;
	
	// 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.	
	configArray['version'] = "unknown version";
	this.moduleList[configArray['name']] = configArray;
	
	return true;
}




/** Mozile - Register Module -
 * Registers the version of a module with the moduleList. The MozileloadModule() method must be called first, in order to put the module on the Mozile.moduleList array. 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();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.registerModule()";
	this.debug(f,1,"Registering module "+ moduleName +" version "+ versionString);
	
	// Set the version string in the this.moduleList array.
	if(this.moduleList[moduleName]) {
		var configArray = this.moduleList[moduleName];
		configArray['version'] = versionString;
		return true;
	}
	// If the module couldn't be found on the list, return false. 
	else {
		return false;
	}
	
}



/** 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();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.unregisterModule()";
	this.debug(f,1,"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();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.moduleIsLoaded()";
	this.debug(f,1,"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]['version'];
		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();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.checkRequirements()";
	this.debug(f,1,"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();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.parseConfig()";
	this.debug(f,1,"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();
	f["File"] = "core/core.js";
	f["Function"] = "Mozile.parseOptions()";
	this.debug(f,1,"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 - Test Function -
 * This function is only used for testing. It gets overridden in the TestModules.
 *
 * @return Always 0.
 */
// 
Mozile.prototype.testFunction = function() {
	return 0;
}



Mozile.prototype.testAlert = function() {
	try{
		netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect UniversalBrowserWrite ");
		alert("Mozile Server-Side Privileged! Extension: "+ this.extension);
	}	catch(e){
		alert("Mozile Server-Side Unprivileged! \n"+e);
	}
}


/**** Activate Configuration ****/
// If the "mozileInterface" variable is set to "false", then we are ready to call the mozileConfiguration() function, which will create a Mozile instance and load modules. 
// If the "mozileInterface" variable is not "false", then more code will be loaded before the configuration is called, so we do nothing here.
try {
	if(mozileInterface == false) mozileConfiguration();
}
catch(e) {
	alert("Error in core.js when calling mozileConfiguration: "+e);
}




/**** UTILITY FUNCTIONS ****/
// These functions are used for development, and should not be relied upon in production code.


/** 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 Oct 19 19:25:33 2005