]> git.donarmstrong.com Git - x_full.git/blobdiff - .mozilla/firefox/default/extensions/itsalltext@docwhat.gerf.org/chrome/content/itsalltext.js
add itsalltext
[x_full.git] / .mozilla / firefox / default / extensions / itsalltext@docwhat.gerf.org / chrome / content / itsalltext.js
diff --git a/.mozilla/firefox/default/extensions/itsalltext@docwhat.gerf.org/chrome/content/itsalltext.js b/.mozilla/firefox/default/extensions/itsalltext@docwhat.gerf.org/chrome/content/itsalltext.js
new file mode 100644 (file)
index 0000000..e0de08e
--- /dev/null
@@ -0,0 +1,838 @@
+/*
+ *  It's All Text - Easy external editing of web forms.
+ *  Copyright 2006 Christian Höltje
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License or
+ *  any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License along
+ *  with this program; if not, write to the Free Software Foundation, Inc.,
+ *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+// @todo [9] IDEA: dropdown list for charsets (utf-8, western-iso, default)?
+// @todo [3] Have a menu/context menu item for turning on monitoring/watch.
+// @todo [9] Menu item to pick the file to load into a textarea.
+// @todo [9] Hot-keys for editing or opening the context menu.
+
+var ItsAllText = function() {
+    /**
+     * This data is all private, which prevents security problems and it
+     * prevents clutter and collection.
+     * @type Object
+     */
+    var that = this;
+
+    /**
+     * Used for tracking all the all the textareas that we are watching.
+     * @type Hash
+     */
+    that.tracker = {};
+
+    /**
+     * Keeps track of all the refreshes we are running.
+     * @type Array
+     */
+    var cron = [null]; // Eat the 0th position
+
+    /**
+     * A constant, a string used for things like the preferences.
+     * @type String
+     */
+    that.MYSTRING = 'itsalltext';
+
+    /**
+     * A constant, the version number.  Set by the Makefile.
+     * @type String
+     */
+    that.VERSION = '0.7.3';
+
+    /**
+     * A constant, the url to the readme.
+     * @type String
+     */
+    that.README = 'chrome://itsalltext/locale/readme.xhtml';
+
+    /* The XHTML Namespace */
+    that.XHTMLNS = "http://www.w3.org/1999/xhtml";
+
+    /* The XUL Namespace */
+    that.XULNS   = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+
+    var string_bundle = Components.classes["@mozilla.org/intl/stringbundle;1"].
+        getService(Components.interfaces.nsIStringBundleService);
+    /**
+     * A localization bundle.  Use it like so:
+     * ItsAllText.locale.getStringFromName('blah');
+     */
+    that.locale = string_bundle.createBundle("chrome://itsalltext/locale/itsalltext.properties");
+    /**
+     * Formats a locale string, replacing $N with the arguments in arr.
+     * @param {String} name Locale property name
+     * @param {Array} arr Array of strings to replace in the string.
+     * @returns String
+     */
+    that.localeFormat = function(name, arr) {
+        return this.locale.formatStringFromName(name, arr, arr.length);
+    };
+    /**
+     * Returns the locale string matching name.
+     * @param {String} name Locale property name
+     * @returns String
+     */
+    that.localeString = function(name) {
+        return this.locale.GetStringFromName(name);
+    };
+
+    /**
+     * Create an error message from given arguments.
+     * @param {Object} message One or more objects to be made into strings...
+     */
+    that.logString = function() {
+        var args = Array.prototype.slice.apply(arguments,[0]);
+        for (var i=0; i<args.length; i++) {
+            try {
+                args[i] = args[i].toString();
+            } catch(e) {
+                Components.utils.reportError(e);
+                args[i] = 'toStringFailed';
+            }
+        }
+        args.unshift(that.MYSTRING+':');
+        return args.join(' ');
+    };
+
+    /**
+     * This is a handy debug message.  I'll remove it or disable it when
+     * I release this.
+     * @param {Object} message One or more objects can be passed in to display.
+     */
+    that.log = function() {
+        var message = that.logString.apply(that, arguments);
+        var consoleService, e;
+        try {
+            // idiom: Convert arguments to an array for easy handling.
+            consoleService = Components.
+                classes["@mozilla.org/consoleservice;1"].
+                getService(Components.interfaces.nsIConsoleService);
+            consoleService.logStringMessage(message);
+        } catch(e) {
+            Components.utils.reportError(message);
+        }
+    };
+
+    /**
+     * Uses log iff debugging is turned on.  Used for messages that need to
+     * globally logged (firebug only logs locally).
+     * @param {Object} message One or more objects can be passed in to display.
+     */
+    that.debuglog = function() {
+        if (that.preferences.debug) {
+            that.log.apply(that,arguments);
+        }
+    };
+
+    /**
+     * Displays debug information, if debugging is turned on.
+     * Requires Firebug.
+     * @param {Object} message One or more objects can be passed in to display.
+     */
+    that.debug = function() {
+        if (that.preferences.debug) {
+            try { Firebug.Console.logFormatted(arguments); } 
+            catch(e) {
+                that.log.apply(that,arguments);
+            }
+        }
+    };
+
+    /**
+     * A factory method to make an nsILocalFile object.
+     * @param {String} path A path to initialize the object with (optional).
+     * @returns {nsILocalFile}
+     */
+    that.factoryFile = function(path) {
+        var file = Components.
+            classes["@mozilla.org/file/local;1"].
+            createInstance(Components.interfaces.nsILocalFile);
+        if (typeof(path) == 'string' && path !== '') {
+            file.initWithPath(path);
+        }
+        return file;
+    };
+
+    /**
+     * Returns the directory where we put files to edit.
+     * @returns nsILocalFile The location where we should write editable files.
+     */
+    that.getEditDir = function() {
+        /* Where is the directory that we use. */
+        var fobj = Components.classes["@mozilla.org/file/directory_service;1"].
+            getService(Components.interfaces.nsIProperties).
+            get("ProfD", Components.interfaces.nsIFile);
+        fobj.append(that.MYSTRING);
+        if (!fobj.exists()) {
+            fobj.create(Components.interfaces.nsIFile.DIRECTORY_TYPE,
+                        parseInt('0700',8));
+        }
+        if (!fobj.isDirectory()) {
+            that.error(that.localeFormat('problem_making_directory', [fobj.path]));
+        }
+        return fobj;
+    };
+
+    /**
+     * Cleans out the edit directory, deleting all old files.
+     */
+    that.cleanEditDir = function(force) {
+        force = (force && typeof(force) != 'undefined');
+        var last_week = Date.now() - (1000*60*60*24*7);
+        var fobj = that.getEditDir();
+        var entries = fobj.directoryEntries;
+        var entry;
+        while (entries.hasMoreElements()) {
+            entry = entries.getNext();
+            entry.QueryInterface(Components.interfaces.nsIFile);
+            if(force || !entry.exists() || entry.lastModifiedTime < last_week){
+                try{
+                    entry.remove(false);
+                } catch(e) {
+                    that.debug('unable to remove',entry,'because:',e);
+                }
+            }
+        }
+    };
+
+    /* Clean the edit directory whenever we create a new window. */
+    that.cleanEditDir();
+
+    /* Load the various bits needed to make this work. */
+    var loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"].getService(Components.interfaces.mozIJSSubScriptLoader);
+    loader.loadSubScript('chrome://itsalltext/content/Color.js', that);
+    loader.loadSubScript('chrome://itsalltext/content/cacheobj.js', that);
+
+    /**
+     * Dictionary for storing the preferences in.
+     * @type Hash
+     */
+    that.preferences = {
+        debug: true,
+
+        /**
+         * Fetches the current value of the preference.
+         * @private
+         * @param {String} aData The name of the pref to fetch.
+         * @returns {Object} The value of the preference.
+         */
+        _get: function(aData) {
+            var po = that.preference_observer;
+            return po._branch['get'+(po.types[aData])+'Pref'](aData);
+        },
+
+        /**
+         * Sets the current preference.
+         * @param {String} aData The name of the pref to change.
+         * @param {Object} value The value to set.
+         */
+        _set: function(aData, value) {
+            var po = that.preference_observer;
+            return po._branch['set'+(po.types[aData])+'Pref'](aData, value);
+        }
+    };
+
+    /**
+     * A Preference Observer.
+     */
+    that.preference_observer = {
+        /**
+         * Dictionary of types (well, really the method needed to get/set the
+         * type.
+         * @type Hash
+         */
+        types: {
+            charset:            'Char',
+            editor:             'Char',
+            refresh:            'Int',
+            debug:              'Bool',
+            disable_gumdrops:   'Bool',
+            extensions:         'Char'
+        },
+
+        /**
+         * Register the observer.
+         */
+        register: function() {
+            var prefService = Components.
+                classes["@mozilla.org/preferences-service;1"].
+                getService(Components.interfaces.nsIPrefService);
+            this._branch = prefService.getBranch("extensions."+that.MYSTRING+".");
+            this._branch.QueryInterface(Components.interfaces.nsIPrefBranch2);
+            this._branch.addObserver("", this, false);
+            /* setup the preferences */
+            for(var type in this.types) {
+                that.preferences[type] = that.preferences._get(type);
+            }
+        },
+
+        /**
+         * Unregister the observer. Not currently used, but may be
+         * useful in the future.
+         */
+        unregister: function() {
+            if (!this._branch) {return;}
+            this._branch.removeObserver("", this);
+        },
+
+        /**
+         * Observation callback.
+         * @param {String} aSubject The nsIPrefBranch we're observing (after appropriate QI)e
+         * @param {String} aData The name of the pref that's been changed (relative to the aSubject).
+         * @param {String} aTopic The string defined by NS_PREFBRANCH_PREFCHANGE_TOPIC_ID
+         */
+        observe: function(aSubject, aTopic, aData) {
+            if (aTopic != "nsPref:changed") {return;}
+            if (that.preferences) {
+                that.preferences[aData] = that.preferences._get(aData);
+                if (aData == 'refresh') {
+                    that.monitor.restart();
+                }
+            }
+        }        
+    };
+
+    /**
+     * A Preference Option: What character set should the file use?
+     * @returns {String} the charset to be used.
+     */
+    that.getCharset = function() {
+        return that.preferences.charset;
+    };
+
+    /**
+     * A Preference Option: How often should we search for new content?
+     * @returns {int} The number of seconds between checking for new content.
+     */
+    that.getRefresh = function() {
+        var refresh = that.preferences.refresh;
+        if (!refresh || refresh < 1) {
+            that.debug('Invalid refresh gotten:',refresh);
+            refresh = 1;
+        }
+        var retval = 1000*refresh;
+        return retval;
+
+    };
+
+    /**
+     * Returns true if the system is running Mac OS X.
+     * @returns {boolean} Is this a Mac OS X system?
+     */
+    that.isDarwin = function() {
+        /* more help:
+         http://developer.mozilla.org/en/docs/Code_snippets:Miscellaneous#Operating_system_detection
+        */
+
+        var is_darwin = that._is_darwin;
+        if (typeof(is_darwin) == 'undefined') {
+            is_darwin = /^Darwin/i.test(Components.classes["@mozilla.org/xre/app-info;1"].getService(Components.interfaces.nsIXULRuntime).OS);
+            that._is_darwin = is_darwin;
+        }
+        return is_darwin;
+    };
+
+    /**
+     * A Preference Option: What editor should we use?
+     *
+     * Note: On some platforms, this can return an 
+     * NS_ERROR_FILE_INVALID_PATH exception and possibly others.
+     *
+     * For a complete list of exceptions, see:
+     * http://lxr.mozilla.org/seamonkey/source/xpcom/base/nsError.h#262
+     * @returns {nsILocalFile} A file object of the editor.
+     */
+    that.getEditor = function() {
+        var editor = that.preferences.editor;
+        var retval = null;
+
+        if (editor === '' && that.isDarwin()) {
+            editor = '/usr/bin/open'; 
+            that.preferences._set('editor', editor);
+        }
+
+        if (editor !== '') {
+            retval = that.factoryFile(editor);
+        }
+        return retval;
+    };
+
+    /**
+     * A Preference Option: should we display debugging info?
+     * @returns {bool}
+     */
+    that.getDebug = function() {
+        return that.preferences.debug;
+    };
+
+    /**
+     * A Preference Option: Are the edit gumdrops disabled?
+     * @returns {bool}
+     */
+    that.getDisableGumdrops = function() {
+        return that.preferences.disable_gumdrops;
+    };
+
+    /**
+     * A Preference Option: The list of extensions
+     * @returns Array
+     */
+    that.getExtensions = function() {
+        var string = that.preferences.extensions.replace(/[\n\t ]+/g,'');
+        var extensions = string.split(',');
+        if (extensions.length === 0) {
+            return ['.txt'];
+        } else {
+            return extensions;
+        }
+    };
+    
+    /**
+     * Open the preferences dialog box.
+     * @param{boolean} wait The function won't return until the preference is set.
+     * @private
+     * Borrowed from http://wiki.mozilla.org/XUL:Windows
+     * and utilityOverlay.js's openPreferences()
+     */
+    that.openPreferences = function (wait) {
+        wait = typeof(wait)=='boolean'?wait:false;
+        var paneID = that.MYSTRING + '_preferences';
+        var instantApply = getBoolPref("browser.preferences.instantApply", false) && !wait;
+        var features = "chrome,titlebar,toolbar,centerscreen" + (instantApply ? ",dialog=no" : ",modal");
+
+        var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].getService(Components.interfaces.nsIWindowMediator);
+        var win = wm.getMostRecentWindow("Browser:Preferences");
+        var pane;
+        if (win) {
+            win.focus();
+            if (paneID) {
+                pane = win.document.getElementById(paneID);
+                win.document.documentElement.showPane(pane);
+            }
+        } else {
+            openDialog('chrome://itsalltext/chrome/preferences.xul',
+                       "", features, paneID);
+        }
+    };
+
+    /**
+     * A Preference Option: Append an extension
+     * @returns Array
+     */
+    that.appendExtensions = function(ext) {
+        ext = ext.replace(/[\n\t ]+/g,'');
+        var current = that.getExtensions();
+        for(var i=0; i<current.length; i++) {
+            if(ext == current[i]) {
+                return; // Don't add a duplicate.
+            }
+        }
+        
+        var value = that.preferences.extensions;
+        if(value.replace(/[\t\n ]+/g) === '') {
+            value = ext;
+        } else {
+            value = [value,',',ext].join('');
+        }
+        that.preferences._set('extensions', value);
+    };
+
+    // @todo [3] Profiling and optimization.
+    
+    /**
+     * Returns a cache object
+     * Note: These UIDs are only unique for Its All Text.
+     * @param {Object} node A dom object node or ID to one.
+     * @returns {String} the UID or null.
+     */
+    that.getCacheObj = function(node) {
+        var cobj = null;
+        var str = that.MYSTRING+"_UID";
+        if (typeof(node) == 'string') {
+            cobj = that.tracker[node];
+        } else {
+            if (node && node.hasAttribute(str)) {
+                cobj = that.tracker[node.getAttribute(str)];
+            }
+            if (!cobj) {
+                cobj = new ItsAllText.CacheObj(node);
+            }
+        }
+        return cobj;
+    };
+
+    /**
+     * Cleans out all old cache objects.
+     */
+    that.cleanCacheObjs = function() {
+        var count = 0;
+        var cobj, id;
+        for(id in that.tracker) {
+            cobj = that.tracker[id];
+            if (cobj.node.ownerDocument.location === null) {
+                that.debug('cleaning %s', id);
+                delete cobj.node;
+                delete cobj.button;
+                delete that.tracker[id];
+            } else {
+                count += 1;
+            }
+        }
+        that.debuglog('tracker count:', count);
+    };
+
+    /**
+     * Refresh Textarea.
+     * @param {Object} node A specific textarea dom object to update.
+     */
+    that.refreshTextarea = function(node, is_chrome) {
+        var cobj = ItsAllText.getCacheObj(node);
+        if(!cobj) { return; }
+
+        cobj.update();
+        if (!is_chrome) { cobj.addGumDrop(); }
+    };
+
+    // @todo [5] Refresh textarea on editor quit.
+    // @todo [9] IDEA: support for input elements as well?
+
+    /**
+     * Refresh Document.
+     * @param {Object} doc The document to refresh.
+     */
+    that.refreshDocument = function(doc) {
+        if(!doc.location) { return; } // it's being cached, but not shown.
+        var is_chrome = (doc.location.protocol == 'chrome:' &&
+                         doc.location.href != that.README);
+        var nodes = doc.getElementsByTagName('textarea');
+        var i;
+        for(i=0; i < nodes.length; i++) {
+            that.refreshTextarea(nodes[i], is_chrome);
+        }
+        nodes = doc.getElementsByTagName('textbox');
+        for(i=0; i < nodes.length; i++) {
+            that.refreshTextarea(nodes[i], is_chrome);
+        }
+    };
+
+    /**
+     * Returns the offset from the containing block.
+     * @param {Object} node A DOM element.
+     * @param {Object} container If unset, then this will use the offsetParent of node. Pass in null to go all the way to the root.
+     * @return {Array} The X & Y page offsets
+     */
+    that.getContainingBlockOffset = function(node, container) {
+        if(typeof(container) == 'undefined') {
+            container = node.offsetParent;
+        }
+        var pos = [node.offsetLeft, node.offsetTop];
+        var pnode = node.offsetParent;
+        while(pnode && (container === null || pnode != container)) {
+            pos[0] += pnode.offsetLeft || 0;
+            pos[1] += pnode.offsetTop  || 0;
+            pos[0] -= pnode.scrollLeft || 0;
+            pos[1] -= pnode.scrollTop  || 0;
+            pnode = pnode.offsetParent;
+        }
+        return pos;
+    };
+
+
+    /**
+     * This function is called regularly to watch changes to web documents.
+     */
+    that.monitor = {
+        id: null,
+        last_now:0,
+        documents: [],
+        /**
+         * Starts or restarts the document monitor.
+         */
+        restart: function() {
+            var rate = that.getRefresh();
+            var id   = that.monitor.id;
+            if (id) {
+                clearInterval(id);
+            }
+            that.monitor.id = setInterval(that.monitor.watcher, rate);
+        },
+        /**
+         * watches the document 'doc'.
+         * @param {Object} doc The document to watch.
+         */
+        watch: function(doc, force) {
+            var contentType, location, is_html, is_usable, is_my_readme;
+            if (!force) {
+                /* Check that this is a document we want to play with. */
+                contentType = doc.contentType;
+                location = doc.location;
+                is_html = (contentType=='text/html' ||
+                           contentType=='text/xhtml' ||
+                           contentType=='application/xhtml+xml');
+                //var is_xul=(contentType=='application/vnd.mozilla.xul+xml');
+                is_usable = (is_html) && 
+                    location.protocol != 'about:' &&
+                    location.protocol != 'chrome:';
+                is_my_readme = location.href == that.README;
+                if (!(is_usable || is_my_readme)) { 
+                    that.debuglog('watch(): ignoring -- ',
+                                  location, contentType);
+                    return;
+                }
+            }
+
+            that.refreshDocument(doc);
+            that.monitor.documents.push(doc);
+        },
+        /**
+         * Callback to be used by restart()
+         * @private
+         */
+        watcher: function(offset) {
+            var monitor = that.monitor;
+            var rate = that.getRefresh();
+            
+            var now = Date.now();
+            if (now - monitor.last_now < Math.round(rate * 0.9)) {
+                that.debuglog('monitor.watcher(',offset,') -- skipping catchup refresh');
+                return;
+            }
+            monitor.last_now = now;
+
+            /* Walk the documents looking for changes */
+            var documents = monitor.documents;
+            that.debuglog('monitor.watcher(',offset,'): ', documents.length);
+            var i, doc;
+            var did_delete = false;
+            for(i in documents) {
+                doc = documents[i];
+                if (doc.location) {
+                    that.debuglog('refreshing', doc.location);
+                    that.refreshDocument(doc);
+                }
+            }
+        },
+        /**
+         * Stops watching doc.
+         * @param {Object} doc The document to watch.
+         */
+        unwatch: function(doc) {
+            var documents = that.monitor.documents;
+            var i;
+            for(i in documents) {
+                if (documents[i] === doc) {
+                    that.debug('unwatching', doc);
+                    delete documents[i];
+                }
+            }
+            that.cleanCacheObjs();
+            for(i=documents.length - 1; i >= 0; i--) {
+                if(typeof(documents[i]) == 'undefined') {
+                    documents.splice(i,1);
+                }
+            }
+        }
+    };
+
+    /**
+     * Callback whenever the DOM content in a window or tab is loaded.
+     * @param {Object} event An event passed in.
+     */
+    that.onDOMContentLoad = function(event) {
+        if (event.originalTarget.nodeName != "#document") { return; }
+        var doc = event.originalTarget || document;
+        that.monitor.watch(doc);
+        return;
+    };
+
+    /**
+     * Open the editor for a selected node.
+     * @param {Object} node The textarea to get.
+     */
+    that.onEditNode = function(node) {
+        var cobj = that.getCacheObj(node);
+        if(cobj) {
+            cobj.edit();
+        }
+        return;
+    };
+
+    /**
+     * Triggered when the context menu is shown.
+     * @param {Object} event The event passed in by the event handler.
+     */
+    that.onContextMenu = function(event) {
+        var tid, node, tag, is_disabled, cobj, menu;
+        if(event.target) {
+            tid = event.target.id;
+            if (tid == "itsalltext-context-popup" ||
+                tid == "contentAreaContextMenu") {
+                node = document.popupNode;
+                tag = node.nodeName.toLowerCase();
+                is_disabled = (!(tag == 'textarea' || 
+                                     tag == 'textbox') ||
+                                   node.style.display == 'none' ||
+                                   node.getAttribute('readonly') ||
+                                   node.getAttribute('disabled')
+                                   );
+                if (tid == "itsalltext-context-popup") {
+                    cobj = that.getCacheObj(node);
+                    that.rebuildMenu(cobj.uid,
+                                     'itsalltext-context-popup',
+                                     is_disabled);
+                } else {
+                    // tid == "contentAreaContextMenu"
+                    menu = document.getElementById("itsalltext-contextmenu");
+                    menu.setAttribute('hidden', is_disabled);
+                }
+                    
+            }
+        }
+        return true;
+    };
+
+    that.openReadme = function() {
+        browser = getBrowser();
+        browser.selectedTab = browser.addTab(that.README, null);
+    };
+
+    /**
+     * Initialize the module.  Should be called once, when a window is loaded.
+     * @private
+     */
+    var windowload = function(event) {
+        that.debug("startup(): It's All Text! is watching this window...");
+
+        // Start watching the preferences.
+        that.preference_observer.register();
+
+        // Start the monitor
+        that.monitor.restart();
+
+        var appcontent = document.getElementById("appcontent"); // The Browser
+        if (appcontent) {
+            // Normal web-page.
+            appcontent.addEventListener("DOMContentLoaded", that.onDOMContentLoad,
+                                        true);
+        } else {
+            that.onDOMContentLoad(event); 
+        }
+        // Attach the context menu, if we can.
+        var contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
+        if (contentAreaContextMenu) {
+            contentAreaContextMenu.addEventListener("popupshowing",
+                                                    that.onContextMenu, false);
+        }
+    };
+  
+    // Do the startup when things are loaded.
+    window.addEventListener("load", windowload, true);
+    // Do the startup when things are unloaded.
+    window.addEventListener("unload", function(event){that.monitor.unwatch(event.originalTarget||document); that.preference_observer.unregister();}, true);
+
+};
+
+/**
+ * The command that is called when picking a new extension.
+ * @param {Event} event
+ */
+ItsAllText.prototype.menuNewExtEdit = function(event) {
+    var that = this;
+    var uid = this._current_uid;
+    var cobj = that.getCacheObj(uid);
+
+    var params = {out:null};       
+    window.openDialog("chrome://itsalltext/chrome/newextension.xul", "",
+    "chrome, dialog, modal, resizable=yes", params).focus();
+    var ext;
+    if (params.out) {
+        ext = params.out.extension.replace(/[\n\t ]+/g,'');
+        if(params.out.do_save) {
+            that.appendExtensions(ext);
+        }
+        cobj.edit(ext);
+    }
+};
+
+/**
+ * The command that is called when selecting an existing extension.
+ * @param {Event} event
+ */
+ItsAllText.prototype.menuExtEdit = function(event) {
+    var that = this;
+    var uid = that._current_uid;
+    var ext = event.target.getAttribute('label');
+    var cobj = that.getCacheObj(uid);
+    cobj.edit(ext);
+};
+
+/**
+ * Rebuilds the option menu, to reflect the current list of extensions.
+ * @private
+ * @param {String} uid The UID to show in the option menu.
+ */
+ItsAllText.prototype.rebuildMenu = function(uid, menu_id, is_disabled) {
+    menu_id = typeof(menu_id) == 'string'?menu_id:'itsalltext-optionmenu';
+    is_disabled = (typeof(is_disabled) == 'undefined'||!is_disabled)?false:(is_disabled&&true);
+    var i;
+    var that = this;
+    var exts = that.getExtensions();
+    var menu = document.getElementById(menu_id);
+    var items = menu.childNodes;
+    var items_length = items.length - 1; /* We ignore the preferences item */
+    var node;
+    that._current_uid = uid;
+    var magic_stop_node = null;
+    var magic_start = null;
+    var magic_stop = null;
+
+    // Find the beginning and end of the magic replacement parts.
+    for(i=0; i<items_length; i++) {
+        node = items[i];
+        if (node.nodeName.toLowerCase() == 'menuseparator') {
+            if(magic_start === null) {
+                magic_start = i;
+            } else if (magic_stop === null) {
+                magic_stop = i;
+                magic_stop_node = node;
+            }
+        } else if (node.nodeName.toLowerCase() == 'menuitem') {
+            node.setAttribute('disabled', is_disabled?'true':'false');
+        }
+    }
+
+    // Remove old magic bits
+    for(i = magic_stop - 1; i > magic_start; i--) {
+        menu.removeChild(items[i]);
+    }
+   
+    // Insert the new magic bits
+    for(i=0; i<exts.length; i++) {
+        node = document.createElementNS(that.XULNS, 'menuitem');
+        node.setAttribute('label', exts[i]);
+        node.addEventListener('command', function(event){return that.menuExtEdit(event);}, false);
+        node.setAttribute('disabled', is_disabled?'true':'false');
+        menu.insertBefore(node, magic_stop_node);
+
+    }
+    return menu;
+};
+
+ItsAllText = new ItsAllText();
+