
/* Merged Plone Javascript file
 * This file is dynamically assembled from separate parts.
 * Some of these parts have 3rd party licenses or copyright information attached
 * Such information is valid for that section,
 * not for the entire composite file
 * originating files are separated by - filename.js -
 */

/* - kupunoi18n.js - */
window._ = function(msgid, interpolations) {
    /* dummy _ function for systems that don't want to use i18n */
    if (interpolations) {
        for (var id in interpolations) {
            var value = interpolations[id];
            var reg = new RegExp('\\\$\\\{' + id + '\\\}', 'g');
            msgid = msgid.replace(reg, ""+value);
        };
    };
    return msgid;
};


/* - sarissa_ieemu_xpath.js - */
/**
 * ====================================================================
 * About
 * ====================================================================
 * Sarissa cross browser XML library - IE XPath Emulation 
 * @version @sarissa.version@
 * @author: Manos Batsis, mailto: mbatsis at users full stop sourceforge full stop net
 *
 * This script emulates Internet Explorer's selectNodes and selectSingleNode
 * for Mozilla. Associating namespace prefixes with URIs for your XPath queries
 * is easy with IE's setProperty. 
 * USers may also map a namespace prefix to a default (unprefixed) namespace in the
 * source document with Sarissa.setXpathNamespaces
 *
 *
 * ====================================================================
 * Licence
 * ====================================================================
 * Sarissa is free software distributed under the GNU GPL version 2 (see <a href="gpl.txt">gpl.txt</a>) or higher, 
 * GNU LGPL version 2.1 (see <a href="lgpl.txt">lgpl.txt</a>) or higher and Apache Software License 2.0 or higher 
 * (see <a href="asl.txt">asl.txt</a>). This means you can choose one of the three and use that if you like. If 
 * you make modifications under the ASL, i would appreciate it if you submitted those.
 * In case your copy of Sarissa does not include the license texts, you may find
 * them online in various formats at <a href="http://www.gnu.org">http://www.gnu.org</a> and 
 * <a href="http://www.apache.org">http://www.apache.org</a>.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY 
 * KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 
 * WARRANTIES OF MERCHANTABILITY,FITNESS FOR A PARTICULAR PURPOSE 
 * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
if(_SARISSA_HAS_DOM_FEATURE && document.implementation.hasFeature("XPath", "3.0")){
    /**
    * <p>SarissaNodeList behaves as a NodeList but is only used as a result to <code>selectNodes</code>,
    * so it also has some properties IEs proprietery object features.</p>
    * @private
    * @constructor
    * @argument i the (initial) list size
    */
    function SarissaNodeList(i){
        this.length = i;
    };
    /** <p>Set an Array as the prototype object</p> */
    SarissaNodeList.prototype = [0];
    /** <p>Inherit the Array constructor </p> */
    SarissaNodeList.prototype.constructor = Array;
    /**
    * <p>Returns the node at the specified index or null if the given index
    * is greater than the list size or less than zero </p>
    * <p><b>Note</b> that in ECMAScript you can also use the square-bracket
    * array notation instead of calling <code>item</code>
    * @argument i the index of the member to return
    * @returns the member corresponding to the given index
    */
    SarissaNodeList.prototype.item = function(i) {
        return (i < 0 || i >= this.length)?null:this[i];
    };
    /**
    * <p>Emulate IE's expr property
    * (Here the SarissaNodeList object is given as the result of selectNodes).</p>
    * @returns the XPath expression passed to selectNodes that resulted in
    *          this SarissaNodeList
    */
    SarissaNodeList.prototype.expr = "";
    /** dummy, used to accept IE's stuff without throwing errors */
    if(window.XMLDocument && (!XMLDocument.prototype.setProperty)){
        XMLDocument.prototype.setProperty  = function(x,y){};
    };
    /**
    * <p>Programmatically control namespace URI/prefix mappings for XPath
    * queries.</p>
    * <p>This method comes especially handy when used to apply XPath queries
    * on XML documents with a default namespace, as there is no other way
    * of mapping that to a prefix.</p>
    * <p>Using no namespace prefix in DOM Level 3 XPath queries, implies you
    * are looking for elements in the null namespace. If you need to look
    * for nodes in the default namespace, you need to map a prefix to it
    * first like:</p>
    * <pre>Sarissa.setXpathNamespaces(oDoc, &quot;xmlns:myprefix=&amp;aposhttp://mynsURI&amp;apos&quot;);</pre>
    * <p><b>Note 1 </b>: Use this method only if the source document features
    * a default namespace (without a prefix), otherwise just use IE's setProperty
    * (moz will rezolve non-default namespaces by itself). You will need to map that
    * namespace to a prefix for queries to work.</p>
    * <p><b>Note 2 </b>: This method calls IE's setProperty method to set the
    * appropriate namespace-prefix mappings, so you dont have to do that.</p>
    * @param oDoc The target XMLDocument to set the namespace mappings for.
    * @param sNsSet A whilespace-seperated list of namespace declarations as
    *            those would appear in an XML document. E.g.:
    *            <code>&quot;xmlns:xhtml=&apos;http://www.w3.org/1999/xhtml&apos;
    * xmlns:&apos;http://www.w3.org/1999/XSL/Transform&apos;&quot;</code>
    * @throws An error if the format of the given namespace declarations is bad.
    */
    Sarissa.setXpathNamespaces = function(oDoc, sNsSet) {
        //oDoc._sarissa_setXpathNamespaces(sNsSet);
        oDoc._sarissa_useCustomResolver = true;
        var namespaces = sNsSet.indexOf(" ")>-1?sNsSet.split(" "):[sNsSet];
        oDoc._sarissa_xpathNamespaces = [namespaces.length];
        for(var i=0;i < namespaces.length;i++){
            var ns = namespaces[i];
            var colonPos = ns.indexOf(":");
            var assignPos = ns.indexOf("=");
            if(colonPos > 0 && assignPos > colonPos+1){
                var prefix = ns.substring(colonPos+1, assignPos);
                var uri = ns.substring(assignPos+2, ns.length-1);
                oDoc._sarissa_xpathNamespaces[prefix] = uri;
            }else{
                throw "Bad format on namespace declaration(s) given";
            };
        };
    };
    /**
    * @private Flag to control whether a custom namespace resolver should
    *          be used, set to true by Sarissa.setXpathNamespaces
    */
    XMLDocument.prototype._sarissa_useCustomResolver = false;
    /** @private */
    XMLDocument.prototype._sarissa_xpathNamespaces = [];
    /**
    * <p>Extends the XMLDocument to emulate IE's selectNodes.</p>
    * @argument sExpr the XPath expression to use
    * @argument contextNode this is for internal use only by the same
    *           method when called on Elements
    * @returns the result of the XPath search as a SarissaNodeList
    * @throws An error if no namespace URI is found for the given prefix.
    */
    XMLDocument.prototype.selectNodes = function(sExpr, contextNode, returnSingle){
        var nsDoc = this;
        var nsresolver = this._sarissa_useCustomResolver?
            function(prefix) {
            var s = nsDoc._sarissa_xpathNamespaces[prefix];
            if(s)return s;
            else throw "No namespace URI found for prefix: '" + prefix+"'";
            }:
            this.createNSResolver(this.documentElement);
        var result = null;
        if(!returnSingle){
            var oResult = this.evaluate(sExpr,
                (contextNode?contextNode:this),
                nsresolver,
                XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
            var nodeList = new SarissaNodeList(oResult.snapshotLength);
            nodeList.expr = sExpr;
            for(var i=0;i<nodeList.length;i++) {
                nodeList[i] = oResult.snapshotItem(i);
            }
            result = nodeList;
        }
        else {
            result = oResult = this.evaluate(sExpr,
                (contextNode?contextNode:this),
                nsresolver,
                XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
        };
        return result;      
    };
    /**
    * <p>Extends the Element to emulate IE's selectNodes</p>
    * @argument sExpr the XPath expression to use
    * @returns the result of the XPath search as an (Sarissa)NodeList
    * @throws An
    *             error if invoked on an HTML Element as this is only be
    *             available to XML Elements.
    */
    Element.prototype.selectNodes = function(sExpr){
        var doc = this.ownerDocument;
        if(doc.selectNodes) {
            return doc.selectNodes(sExpr, this);
        }
        else {
            throw "Method selectNodes is only supported by XML Elements";
        }
    };
    /**
    * <p>Extends the XMLDocument to emulate IE's selectSingleNode.</p>
    * @argument sExpr the XPath expression to use
    * @argument contextNode this is for internal use only by the same
    *           method when called on Elements
    * @returns the result of the XPath search as an (Sarissa)NodeList
    */
    XMLDocument.prototype.selectSingleNode = function(sExpr, contextNode){
        var ctx = contextNode?contextNode:null;
        return this.selectNodes(sExpr, ctx, true);
    };
    /**
    * <p>Extends the Element to emulate IE's selectSingleNode.</p>
    * @argument sExpr the XPath expression to use
    * @returns the result of the XPath search as an (Sarissa)NodeList
    * @throws An error if invoked on an HTML Element as this is only be
    *             available to XML Elements.
    */
    Element.prototype.selectSingleNode = function(sExpr){
        var doc = this.ownerDocument;
        if(doc.selectSingleNode) {
            return doc.selectSingleNode(sExpr, this);
        } else {
            throw "Method selectNodes is only supported by XML Elements";
        }
    };
    Sarissa.IS_ENABLED_SELECT_NODES = true;
}

/* - kupuhelpers.js - */
/*****************************************************************************
 *
 * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/
// $Id: kupuhelpers.js 59576 2008-10-30 16:21:37Z guido $

/*

Some notes about the scripts:

- Problem with bound event handlers:
    
    When a method on an object is used as an event handler, the method uses 
    its reference to the object it is defined on. The 'this' keyword no longer
    points to the class, but instead refers to the element on which the event
    is bound. To overcome this problem, you can wrap the method in a class that
    holds a reference to the object and have a method on the wrapper that calls
    the input method in the input object's context. This wrapped method can be
    used as the event handler. An example:

    class Foo() {
        this.foo = function() {
            // the method used as an event handler
            // using this here wouldn't work if the method
            // was passed to addEventListener directly
            this.baz();
        };
        this.baz = function() {
            // some method on the same object
        };
    };

    f = new Foo();

    // create the wrapper for the function, args are func, context
    wrapper = new ContextFixer(f.foo, f);

    // the wrapper can be passed to addEventListener, 'this' in the method
    // will be pointing to the right context.
    some_element.addEventListener("click", wrapper.execute, false);

- Problem with window.setTimeout:

    The window.setTimeout function has a couple of problems in usage, all 
    caused by the fact that it expects a *string* argument that will be
    evalled in the global namespace rather than a function reference with
    plain variables as arguments. This makes that the methods on 'this' can
    not be called (the 'this' variable doesn't exist in the global namespace)
    and references to variables in the argument list aren't allowed (since
    they don't exist in the global namespace). To overcome these problems, 
    there's now a singleton instance of a class called Timer, which has one 
    public method called registerFunction. This can be called with a function
    reference and a variable number of extra arguments to pass on to the 
    function.

    Usage:

        timer_instance.registerFunction(this, this.myFunc, 10, 'foo', bar);

        will call this.myFunc('foo', bar); in 10 milliseconds (with 'this'
        as its context).

*/

//----------------------------------------------------------------------------
// Helper classes and functions
//----------------------------------------------------------------------------
function newDocumentElement(doc, tagName, args) {
    /* Create a new element, set attributes, and append children */
    if (_SARISSA_IS_IE) {
        /* Braindead IE cannot set some attributes (e.g. NAME) except
         * through bizarre use of createElement */
        var attrs = [tagName];
        for (var a = 1; a < args.length; a++) {
            var arg = args[a];
            if (arg.length===undefined) {
                for (var attr in arg) {
                    var val = arg[attr];
                    if (val===true) val=attr;
                    if (val===false) continue;
                    if (attr=='className') attr='class';
                    attrs.push(attr+'="'+val.replace(/"/,'&quot;')+'"');
                };
            };
        };
        tagName = "<"+attrs.join(' ')+"></"+tagName+">";
    }
    var node = doc.createElement(tagName);
    for (var a = 1; a < args.length; a++) {
        var arg = args[a];
        if (arg.length===undefined) {
            if (!_SARISSA_IS_IE) {
                for (var attr in arg) {
                    if (/^on/.test(attr)) {
                        node.setAttribute(attr, arg[attr]);
                    } else {
                        node[attr] = arg[attr];
                    };
                };
            };
        } else {
            for (var i = 0; i < arg.length; i++) {
                if(typeof(arg[i])=='string') {
                    node.appendChild(doc.createTextNode(arg[i]));
                } else {
                    node.appendChild(arg[i]);
                }
            }
        }
    }
    return node;
}

function newElement(tagName) {
    return newDocumentElement(document, tagName, arguments);
}

function addEventHandler(element, event, method, context) {
    /* method to add an event handler for both IE and Mozilla */
    var wrappedmethod = new ContextFixer(method, context);
    var args = [null, null];
    for (var i=4; i < arguments.length; i++) {
        args.push(arguments[i]);
    };
    wrappedmethod.args = args;
    try {
        if (element.addEventListener) {
            element.addEventListener(event, wrappedmethod.execute, false);
        } else if (element.attachEvent) {
            element.attachEvent("on" + event, wrappedmethod.execute);
        } else {
            throw _("Unsupported browser!");
        };
        return wrappedmethod.execute;
    } catch(e) {
        var msg = _(
            'exception ${message} while registering an event handler ' +
            'for element ${element}, event ${event}, method ${method}, ',
            {'message': e.message, 'element': element,
                'event': event,
                'method': method
            });
        if (e.stack) {
            msg += _('\r\ntraceback:\r\n${traceback}', {'traceback': e.stack});
        };
        alert(msg);
    };
};

function removeEventHandler(element, event, method) {
    /* method to remove an event handler for both IE and Mozilla */
    if (element.removeEventListener) {
        element.removeEventListener(event, method, false);
    } else if (element.detachEvent) {
        element.detachEvent("on" + event, method);
    } else {
        throw _("Unsupported browser!");
    };
};

/* Replacement for window.document.getElementById()
 * selector can be an Id (so we maintain backwards compatability)
 * but is intended to be a subset of valid CSS selectors.
 * For now we only support the format: "#id tag.class"
 */
function getFromSelector(selector) {
    var match = /#(\S+)\s*([^ .]+)\.(\S+)/.exec(selector);
    if (!match) {
        return window.document.getElementById(selector);
    }
    var id=match[1], tag=match[2], className=match[3];
    var base = window.document.getElementById(id);
    return getBaseTagClass(base, tag, className);
}

function getBaseTagClass(base, tag, className) {
    var classPat = new RegExp('\\b'+className+'\\b');

    var nodes = base.getElementsByTagName(tag);
    for (var i = 0; i < nodes.length; i++) {
        if (classPat.test(nodes[i].className)) {
            return nodes[i];
        }
    }
    return null;
}

function openPopup(url, width, height, properties) {
    /* open and center a popup window */
    var allprops = 'width=' + width + ',height=' + height;
    if (properties) {
        allprops += ',' + properties;
    };
    var win = window.open(url, 'someWindow', allprops);
    return win;
};

function selectSelectItem(select, item) {
    /* select a certain item from a select */
    for (var i=0; i < select.options.length; i++) {
        var option = select.options[i];
        if (option.value == item) {
            select.selectedIndex = i;
            return;
        }
    }
    select.selectedIndex = 0;
};

function parentWithStyleChecker(tagnames, style, stylevalue, command) {
    /* small wrapper that provides a generic function to check if a
       button should look pressed in */
    return function(selNode, button, editor, event) {
        /* check if the button needs to look pressed in */
        if (command) {
            var result = editor.getInnerDocument().queryCommandState(command);
            if (result || editor.getSelection().getContentLength() == 0) {
                return result;
            };
        };
        var currnode = selNode;
        while (currnode && currnode.style) {
            for (var i=0; i < tagnames.length; i++) {
                if (currnode.nodeName.toLowerCase() == tagnames[i].toLowerCase()) {
                    return true;
                };
            };
            if (style && currnode.style[style] == stylevalue) {
                return true;
            };
            currnode = currnode.parentNode;
        };
        return false;
    };
};

function _load_dict_helper(element) {
    /* walks through a set of XML nodes and builds a nested tree of objects */
    var dict = {};
    for (var i=0; i < element.childNodes.length; i++) {
        var child = element.childNodes[i];
        if (child.nodeType == 1) {
            var value = '';
            for (var j=0; j < child.childNodes.length; j++) {
                // test if we can recurse, if so ditch the string (probably
                // ignorable whitespace) and dive into the node
                if (child.childNodes[j].nodeType == 1) {
                    value = _load_dict_helper(child);
                    break;
                } else if (typeof(value) == typeof('')) {
                    value += child.childNodes[j].nodeValue;
                };
            };
            if (typeof(value) == typeof('') && !isNaN(parseInt(value)) && 
                    parseInt(value).toString().length == value.length) {
                value = parseInt(value);
            } else if (typeof(value) != typeof('')) {
                if (value.length == 1) {
                    value = value[0];
                };
            };
            var name = child.nodeName.toLowerCase();
            var attr = child.attributes[0];
            if (attr && !(/^([^_]|_moz)/.test(attr.name))) {
                name += attr.name.toLowerCase(); // Fix for Opera
            }
            if (dict[name] != undefined) {
                if (!dict[name].push) {
                    dict[name] = [dict[name], value];
                } else {
                    dict[name].push(value);
                };
            } else {
                dict[name] = value;
            };
        };
    };
    return dict;
};

function loadDictFromXML(document, islandid) {
    /* load configuration values from an XML chunk

        this is quite generic, it just reads data from a chunk of XML into
        an object, checking if the object is complete should be done in the
        calling context.
    */
    var dict = {};
    var confnode = getFromSelector(islandid);
    var root = null;
    for (var i=0; i < confnode.childNodes.length; i++) {
        if (confnode.childNodes[i].nodeType == 1) {
            root = confnode.childNodes[i];
            break;
        };
    };
    if (!root) {
        throw(_('No element found in the config island!'));
    };
    dict = _load_dict_helper(root);
    return dict;
};

function NodeIterator(node, continueatnextsibling) {
    /* simple node iterator

        can be used to recursively walk through all children of a node,
        the next() method will return the next node until either the next
        sibling of the startnode is reached (when continueatnextsibling is 
        false, the default) or when there's no node left (when 
        continueatnextsibling is true)

        returns false if no nodes are left
    */
    this.node = node;
    this.current = node;
    this.terminator = continueatnextsibling ? null : node;
    
    this.next = function() {
        /* return the next node */
        if (this.current === false) {
            // restart
            this.current = this.node;
        };
        var current = this.current;
        if (current.firstChild) {
            this.current = current.firstChild;
        } else {
            // walk up parents until we finish or find one with a nextSibling
            while (current !== this.terminator && !current.nextSibling) {
                current = current.parentNode;
            };
            if (current === this.terminator) {
                this.current = false;
            } else {
                this.current = current.nextSibling;
            };
        };
        return this.current;
    };

    this.reset = function() {
        /* reset the iterator so it starts at the first node */
        this.current = this.node;
    };

    this.setCurrent = function(node) {
        /* change the current node
            
            can be really useful for specific hacks, the user must take
            care that the node is inside the iterator's scope or it will
            go wild
        */
        this.current = node;
    };
};

/* selection classes, these are wrappers around the browser-specific
    selection objects to provide a generic abstraction layer
*/
function BaseSelection() {
    /* superclass for the Selection objects
    
        this will contain higher level methods that don't contain 
        browser-specific code
    */
    this.splitNodeAtSelection = function(node) {
        /* split the node at the current selection

            remove any selected text, then split the node on the location
            of the selection, thus creating a new node, this is attached to
            the node's parent after the node

            this will fail if the selection is not inside the node
        */
        if (!this.selectionInsideNode(node)) {
            throw(_('Selection not inside the node!'));
        };
        // a bit sneaky: what we'll do is insert a new br node to replace
        // the current selection, then we'll walk up to that node in both
        // the original and the cloned node, in the original we'll remove
        // the br node and everything that's behind it, on the cloned one
        // we'll remove the br and everything before it
        // anyway, we'll end up with 2 nodes, the first already in the 
        // document (the original node) and the second we can just attach
        // to the doc after the first one
        var doc = this.document.getDocument();
        var br = doc.createElement('br');
        br.setAttribute('node_splitter', 'indeed');
        this.replaceWithNode(br);
        
        var clone = node.cloneNode(true);

        // now walk through the original node
        var iterator = new NodeIterator(node);
        var currnode = iterator.next();
        var remove = false;
        while (currnode) {
            if (currnode.nodeName.toLowerCase() == 'br' && currnode.getAttribute('node_splitter') == 'indeed') {
                // here's the point where we should start removing
                remove = true;
            };
            // we should fetch the next node before we remove the current one, else the iterator
            // will fail (since the current node is removed)
            var lastnode = currnode;
            currnode = iterator.next();
            // XXX this will leave nodes that *became* empty in place, since it doesn't visit it again,
            // perhaps we should do a second pass that removes the rest(?)
            if (remove && (lastnode.nodeType == 3 || !lastnode.hasChildNodes())) {
                lastnode.parentNode.removeChild(lastnode);
            };
        };

        // and through the clone
        var iterator = new NodeIterator(clone);
        var currnode = iterator.next();
        var remove = true;
        while (currnode) {
            var lastnode = currnode;
            currnode = iterator.next();
            if (lastnode.nodeName.toLowerCase() == 'br' && lastnode.getAttribute('node_splitter') == 'indeed') {
                // here's the point where we should stop removing
                lastnode.parentNode.removeChild(lastnode);
                remove = false;
            };
            if (remove && (lastnode.nodeType == 3 || !lastnode.hasChildNodes())) {
                lastnode.parentNode.removeChild(lastnode);
            };
        };

        // next we need to attach the node to the document
        if (node.nextSibling) {
            node.parentNode.insertBefore(clone, node.nextSibling);
        } else {
            node.parentNode.appendChild(clone);
        };

        // this will change the selection, so reselect
        this.reset();

        // return a reference to the clone
        return clone;
    };

    this.selectionInsideNode = function(node) {
        /* returns a Boolean to indicate if the selection is resided
            inside the node
        */
        var currnode = this.parentElement();
        while (currnode) {
            if (currnode == node) {
                return true;
            };
            currnode = currnode.parentNode;
        };
        return false;
    };
};

function MozillaSelection(document) {
    var win = document.getWindow();
    this.document = document;
    this.selection = win.getSelection();

    this._createRange = function() {
        return this.document.getDocument().createRange();
    };
    this.selectNodeContents = function(node) {
        if (node && node.parentNode) {
            /* select the contents of a node */
            var sel = this.selection;
            sel.removeAllRanges();
            if (sel.selectAllChildren && node.nodeType == 1) {
                sel.selectAllChildren(node);
            } else {
                var range = this._createRange();
                try {
                    range.selectNode(node);
                } catch (e) {
                    range.selectNodeContents(node);
                };
                sel.addRange(range);
            };
        };
    };

    this.collapse = function(collapseToEnd) {
        try {
            if (!this.selection) this.reset();
            if (!collapseToEnd) {
                this.selection.collapseToStart();
            } else {
                this.selection.collapseToEnd();
            };
        } catch(e) {};
    };

    this.replaceWithNode = function(node, selectAfterPlace) {
        // XXX this should be on a range object
        /* replaces the current selection with a new node
            returns a reference to the inserted node 

            newnode is the node to replace the content with, selectAfterPlace
            can either be a DOM node that should be selected after the new
            node was placed, or some value that resolves to true to select
            the placed node
        */
        // get the first range of the selection
        // (there's almost always only one range)
        var range = this.selection.getRangeAt(0);

        // deselect everything
        this.selection.removeAllRanges();

        // remove content of current selection from document
        range.deleteContents();

        // get location of current selection
        var container = range.startContainer;
        var pos = range.startOffset;

        // make a new range for the new selection
        var range = this._createRange();

        if (container.nodeType == 3 && node.nodeType == 3) {
            // if we insert text in a textnode, do optimized insertion
            container.insertData(pos, node.nodeValue);

            // put cursor after inserted text
            range.setEnd(container, pos + node.length);
            range.setStart(container, pos + node.length);
        } else {
            var afterNode;
            if (container.nodeType == 3) {
                // when inserting into a textnode
                // we create 2 new textnodes
                // and put the node in between

                var textNode = container;
                var container = textNode.parentNode;
                var text = textNode.nodeValue;

                // text before the split
                var textBefore = text.substr(0,pos);
                // text after the split
                var textAfter = text.substr(pos);

                var beforeNode = this.document.getDocument().createTextNode(textBefore);
                afterNode = this.document.getDocument().createTextNode(textAfter);

                // insert the 3 new nodes before the old one
                container.insertBefore(afterNode, textNode);
                container.insertBefore(node, afterNode);
                container.insertBefore(beforeNode, node);

                // remove the old node
                container.removeChild(textNode);
            } else {
                // else simply insert the node
                afterNode = container.childNodes[pos];
                if (afterNode) {
                    container.insertBefore(node, afterNode);
                } else {
                    container.appendChild(node);
                    afterNode = container.nextSibling;
                };
            }
            range.setEndAfter(node);
            range.collapse(false);
        }

        if (selectAfterPlace) {
            // a bit implicit here, but I needed this to be backward 
            // compatible and also I didn't want yet another argument,
            // JavaScript isn't as nice as Python in that respect (kwargs)
            // if selectAfterPlace is a DOM node, select all of that node's
            // contents, else select the newly added node's
            this.selection = win.getSelection();
            this.selection.addRange(range);
            if (selectAfterPlace.nodeType == 1) {
                this.selection.selectAllChildren(selectAfterPlace);
            } else {
                if (node.hasChildNodes()) {
                    this.selection.selectAllChildren(node);
                } else {
                    var range = this.selection.getRangeAt(0).cloneRange();
                    this.selection.removeAllRanges();
                    range.selectNode(node);
                    this.selection.addRange(range);
                };
            };
            win.focus();
        };
        return node;
    };

    this.startOffset = function() {
        // XXX this should be on a range object
        var startnode = this.startNode();
        var startnodeoffset = 0;
        if (startnode == this.selection.anchorNode) {
            startnodeoffset = this.selection.anchorOffset;
        } else {
            startnodeoffset = this.selection.focusOffset;
        };
        var parentnode = this.parentElement();
        if (startnode == parentnode) {
            return startnodeoffset;
        };
        var currnode = parentnode.firstChild;
        var offset = 0;
        if (!currnode) {
            // 'Control range', range consists of a single element, so startOffset is 0
            if (startnodeoffset != 0) {
                // just an assertion to see if my assumption about this case is right
                throw(_('Start node offset detected in a node without children!'));
            };
            return 0;
        };
        while (currnode != startnode) {
            if (currnode.nodeType == 3) {
                offset += currnode.nodeValue.length;
            };
            while (!currnode.nextSibling) {
                currnode = currnode.parentNode;
            };
            currnode = currnode.nextSibling;
        };
        return offset + startnodeoffset;
    };

    this.startNode = function() {
        // XXX this should be on a range object
        var anode = this.selection.anchorNode;
        var aoffset = this.selection.anchorOffset;
        var onode = this.selection.focusNode;
        var ooffset = this.selection.focusOffset;
        var arange = this._createRange();
        arange.setStart(anode, aoffset);
        var orange = this._createRange();
        orange.setStart(onode, ooffset);
        return arange.compareBoundaryPoints('START_TO_START', orange) <= 0 ? anode : onode;
    };

    this.endOffset = function() {
        // XXX this should be on a range object
        var endnode = this.endNode();
        var endnodeoffset = 0;
        if (endnode == this.selection.focusNode) {
            endnodeoffset = this.selection.focusOffset;
        } else {
            endnodeoffset = this.selection.anchorOffset;
        };
        var parentnode = this.parentElement();
        var currnode = parentnode.firstChild;
        var offset = 0;
        if (parentnode == endnode) {
            for (var i=0; i < parentnode.childNodes.length; i++) {
                var child = parentnode.childNodes[i];
                if (i == endnodeoffset) {
                    return offset;
                };
                if (child.nodeType == 3) {
                    offset += child.nodeValue.length;
                };
            };
        };
        if (!currnode) {
            // node doesn't have any content, so offset is always 0
            if (endnodeoffset != 0) {
                // just an assertion to see if my assumption about this case is right
                var msg = _('End node offset detected in a node without ' +
                            'children!');
                alert(msg);
                throw(msg);
            };
            return 0;
        };
        while (currnode && currnode != endnode) {
            if (currnode.nodeType == 3) { // should account for CDATA nodes as well
                offset += currnode.nodeValue.length;
            };
            currnode = currnode.nextSibling;
        };
        return offset + endnodeoffset;
    };

    this.endNode = function() {
        // XXX this should be on a range object
        var anode = this.selection.anchorNode;
        var aoffset = this.selection.anchorOffset;
        var onode = this.selection.focusNode;
        var ooffset = this.selection.focusOffset;
        var arange = this._createRange();
        arange.setStart(anode, aoffset);
        var orange = this._createRange();
        orange.setStart(onode, ooffset);
        return arange.compareBoundaryPoints('START_TO_START', orange) > 0 ? anode : onode;
    };

    this.getContentLength = function() {
        // XXX this should be on a range object
        return this.selection.toString().length;
    };

    this.cutChunk = function(startOffset, endOffset) {
        // XXX this should be on a range object
        var range = this.selection.getRangeAt(0);
        
        // set start point
        var offsetParent = this.parentElement();
        var currnode = offsetParent.firstChild;
        var curroffset = 0;

        var startparent = null;
        var startparentoffset = 0;
        
        while (currnode) {
            if (currnode.nodeType == 3) { // XXX need to add CDATA support
                var nodelength = currnode.nodeValue.length;
                if (curroffset + nodelength < startOffset) {
                    curroffset += nodelength;
                } else {
                    startparent = currnode;
                    startparentoffset = startOffset - curroffset;
                    break;
                };
            };
            currnode = currnode.nextSibling;
        };
        // set end point
        var currnode = offsetParent.firstChild;
        var curroffset = 0;

        var endparent = null;
        var endparentoffset = 0;
        
        while (currnode) {
            if (currnode.nodeType == 3) { // XXX need to add CDATA support
                var nodelength = currnode.nodeValue.length;
                if (curroffset + nodelength < endOffset) {
                    curroffset += nodelength;
                } else {
                    endparent = currnode;
                    endparentoffset = endOffset - curroffset;
                    break;
                };
            };
            currnode = currnode.nextSibling;
        };
        
        // now cut the chunk
        if (!startparent) {
            throw(_('Start offset out of range!'));
        };
        if (!endparent) {
            throw(_('End offset out of range!'));
        };

        var newrange = range.cloneRange();
        newrange.setStart(startparent, startparentoffset);
        newrange.setEnd(endparent, endparentoffset);
        return newrange.extractContents();
    };

    this.getElementLength = function(element) {
        // XXX this should be a helper function
        var length = 0;
        var currnode = element.firstChild;
        while (currnode) {
            if (currnode.nodeType == 3) { // XXX should support CDATA as well
                length += currnode.nodeValue.length;
            };
            currnode = currnode.nextSibling;
        };
        return length;
    };

    this.parentElement = function(allowmulti) {
        /* return the selected node (or the node containing the selection) */
        // XXX this should be on a range object
        if (!this.selection) {
            return null;
        }
        if (this.selection.rangeCount == 0) {
            var parent = this.document.getDocument().body;
            while (parent.firstChild) {
                parent = parent.firstChild;
            };
        } else {
            var range = this.selection.getRangeAt(0);
            var parent = this.parentElementOfRange(range);
            if( allowmulti ) {
                var numRanges = this.selection.rangeCount;
                for( var i = 1; i < numRanges; i = i + 1 )
                {
                    var parent1 = parent;
                    var parent2 = null;
                    var range1 = this._createRange();
                    var range2 = this._createRange();
                
                    var parent2 = this.parentElementOfRange(this.selection.getRangeAt(i));

                    range1.selectNode(parent1);
                    range2.selectNode(parent2);
                    
                    if( range1.compareBoundaryPoints(Range.START_TO_START, range2) <= 0 &&
                        range1.compareBoundaryPoints(Range.END_TO_END, range2) >= 0 ) {
                        //parent1 contains parent2
                        parent = parent1;
                    } else if( range1.compareBoundaryPoints(Range.START_TO_START, range2) >= 0 &&
                        range1.compareBoundaryPoints(Range.END_TO_END, range2) <= 0 ) {
                        //parent2 contains parent1
                        parent = parent2;
                    } else if( range1.compareBoundaryPoints(Range.START_TO_END, range2) <= 0 ) {
                        //parent1 comes before parent2
                        //commonAncestorContainer returns the node parent if a range is
                        //just one node, which we don't want; but since parent1
                        //and parent2 are different, their range is not just
                        //one node
                        var coverRange = this._createRange();
                        coverRange.setStartBefore(parent1);
                        coverRange.setEndAfter(parent2);
                        parent = coverRange.commonAncestorContainer;
                    } else {
                        //parent2 comes before parent1
                        //commonAncestorContainer returns the node parent if a range is
                        //just one node, which we don't want; but since parent1
                        //and parent2 are different, their range is not just
                        //one node
                        var coverRange = this._createRange();
                        coverRange.setStartBefore(parent2);
                        coverRange.setEndAfter(parent1);
                        parent = coverRange.commonAncestorContainer;                    
                    };
                };
            };
        };            

        if (parent.nodeType == Node.TEXT_NODE) {
            parent = parent.parentNode;
        };
        return parent;
    };

    this.parentElementOfRange = function(range) {
        if( range.compareBoundaryPoints(Range.START_TO_END, range) < 0 ) {
            var startNode = range.endContainer;
            var startOffset = range.endOffset;
            var endNode = range.startContainer;
            var endOffset = range.startOffset;
            range.setStart( startNode, startOffset );
            range.setEnd( endNode, endOffset );
        }
            
        var parent = range.commonAncestorContainer;
            
        // if there is only a single node selected, e.g. after a click on
        // an image, then this node itself should be returned as the
        // parentElement. however, in this case, "parent" is the selected
        // node's parent. the following searches if any other node
        // intersects the selection range; if not, then the selected node
        // is set to the parentElement.     
        var inv = range.compareBoundaryPoints(Range.START_TO_END, range) < 0;
        var startNode = inv ? range.endContainer : range.startContainer;
        var startOffset = inv ? range.endOffset : range.startOffset;
        var endNode = inv ? range.startContainer : range.endContainer;
        var endOffset = inv ? range.startOffset : range.endOffset;

        var selectedChild = null;
        var child = parent.firstChild;
        while (child) {
            if (range.intersectsNode(child) &&
                !(child == startNode && startOffset == child.length) &&
                !(child == endNode && endOffset == 0)) {
                if (selectedChild) {
                    // current child is the second node found that
                    // intersects the selection, so commonAncestorContainer
                    // is the correct parentElement to use                        
                    selectedChild = null;
                    break;
                } else {
                    // current child is the first selected child found
                    selectedChild = child;
                };
            } else if (selectedChild) {
                // current child is after the selection
                break;
            };
            child = child.nextSibling;
        };

        if (selectedChild) {
            parent = selectedChild;
        };
        if (parent.nodeType == Node.TEXT_NODE) {
            parent = parent.parentNode;
        };

        return parent;
    };

    // deprecated alias of parentElement
    this.getSelectedNode = this.parentElement;

    this.moveStart = function(offset) {
        // XXX this should be on a range object
        var offsetparent = this.parentElement();
        // the offset within the offsetparent
        var startoffset = this.startOffset();
        var realoffset = offset + startoffset;
        if (realoffset >= 0) {
            var currnode = offsetparent.firstChild;
            var curroffset = 0;
            while (currnode) {
                if (currnode.nodeType == 3) { // XXX need to support CDATA sections
                    var nodelength = currnode.nodeValue.length;
                    if (curroffset + nodelength >= realoffset) {
                        var range = this.selection.getRangeAt(0);
                        //range.setEnd(this.endNode(), this.endOffset());
                        range.setStart(currnode, realoffset - curroffset);
                        return;
                        //this.selection.removeAllRanges();
                        //this.selection.addRange(range);
                    };
                };
                currnode = currnode.nextSibling;
            };
            // if we still haven't found the startparent we should walk to 
            // all nodes following offsetparent as well
            var currnode = offsetparent.nextSibling;
            while (currnode) {
                if (currnode.nodeType == 3) {
                    var nodelength = currnode.nodeValue.length;
                    if (curroffset + nodelength >= realoffset) {
                        var range = this.selection.getRangeAt(0);
                        // XXX does IE switch the begin and end nodes here as well?
                        var endnode = this.endNode();
                        var endoffset = this.endOffset();
                        range.setEnd(currnode, realoffset - curroffset);
                        range.setStart(endnode, endoffset);
                        return;
                    };
                    curroffset += nodelength;
                };
                currnode = currnode.nextSibling;
            };
            throw(_('Offset out of document range'));
        } else if (realoffset < 0) {
            var currnode = offsetparent.prevSibling;
            var curroffset = 0;
            while (currnode) {
                if (currnode.nodeType == 3) { // XXX need to support CDATA sections
                    var currlength = currnode.nodeValue.length;
                    if (curroffset - currlength < realoffset) {
                        var range = this.selection.getRangeAt(0);
                        range.setStart(currnode, realoffset - curroffset);
                    };
                    curroffset -= currlength;
                };
                currnode = currnode.prevSibling;
            };
        } else {
            var range = this.selection.getRangeAt(0);
            range.setStart(offsetparent, 0);
            //this.selection.removeAllRanges();
            //this.selection.addRange(range);
        };
    };

    this.moveEnd = function(offset) {
        // XXX this should be on a range object
    };

    this.reset = function() {
        this.selection = win.getSelection();
    };

    this.cloneContents = function() {
        /* returns a document fragment with a copy of the contents */
        var range = this.selection.getRangeAt(0);
        return range.cloneContents();
    };

    this.containsNode = function(node) {
        var sel = this.selection;
        if (sel.containsNode) {
            return sel.containsNode(node, true);
        } else {
            // kludge it for safari
            for(var i = 0; i < sel.rangeCount; i++ ) {
                if( sel.getRangeAt(i).containsNode(node) ) {
                    return true;
                }
            };
            return false;
        }
    };

    this.toString = function() {
        return this.selection.toString();
    };

    this.getRange = function() {
        if (this.selection && this.selection.rangeCount > 0) {
            return this.selection.getRangeAt(0);
        }
    };
    this.restoreRange = function(range) {
        var selection = this.selection;
        if (selection) {
            selection.removeAllRanges();
            selection.addRange(range);
        }
    };

    //sample kindly snipped from Mozilla's wiki
    if( !win.Range.prototype.intersectsNode ){
        win.Range.prototype.intersectsNode = function(node) {
            var nodeRange = node.ownerDocument.createRange();
            try {
                nodeRange.selectNode(node);
            } catch (e) {
                nodeRange.selectNodeContents(node);
            };

            // selection end after node start and selection start
            // before node end
            return this.compareBoundaryPoints(Range.END_TO_START, nodeRange) == -1 &&
                this.compareBoundaryPoints(Range.START_TO_END, nodeRange) == 1;
        };
    };
    this.intersectsNode = function(node) {
        for(var i = 0; i < this.selection.rangeCount; i++ ) {
           if( this.selection.getRangeAt(i).intersectsNode(node) ) {
               return true;
           }
        };
        return false;
    };
    if( !win.Range.prototype.containsNode ){
        win.Range.prototype.containsNode = function(node) {
            var nodeRange = node.ownerDocument.createRange();
            try {
                nodeRange.selectNode(node);
            } catch (e) {
                nodeRange.selectNodeContents(node);
            };

            // selection start not after node start and selection end
            // not before node end.
            return this.compareBoundaryPoints(Range.START_TO_START, nodeRange) != -1 &&
                    this.compareBoundaryPoints(Range.END_TO_END, nodeRange) != 1;
        };
    };
};

MozillaSelection.prototype = new BaseSelection;

function IESelection(document) {
    this.document = document;
    this.selection = document.getDocument().selection;

    /* If no selection in editable document, IE returns selection from
     * main page, so force an inner selection. */
    var doc = document.getDocument();

    var range = this.selection.createRange();
    var parent = this.selection.type=="Text" ?
        range.parentElement() :
        this.selection.type=="Control" ?  range.parentElement : null;

    if(parent && parent.ownerDocument != doc) {
            var range = doc.body.createTextRange();
            range.collapse();
            range.select();
            this.reset();
    }

    this.selectNodeContents = function(node) {
        /* select the contents of a node */
        // a bit nasty, when moveToElementText is called it will move the selection start
        // to just before the element instead of inside it, and since IE doesn't reserve
        // an index for the element itself as well the way to get it inside the element is
        // by moving the start one pos and then moving it back (yuck!)
        var range = doc.body.createTextRange();
        range.moveToElementText(node);
        range.moveStart('character', 1);
        range.moveStart('character', -1);
        range.moveEnd('character', -1);
        range.moveEnd('character', 1);
        range.select();
        this.reset();
    };

    this.collapse = function(collapseToEnd) {
        var range = this.selection.createRange();
        range.collapse(!collapseToEnd);
        range.select();
        this.reset();
    };

    this.replaceWithNode = function(newnode, selectAfterPlace) {
        /* replaces the current selection with a new node
            returns a reference to the inserted node 

            newnode is the node to replace the content with, selectAfterPlace
            can either be a DOM node that should be selected after the new
            node was placed, or some value that resolves to true to select
            the placed node
        */
        if (this.selection.type == 'Control') {
            var range = this.selection.createRange();
            range.item(0).parentNode.replaceChild(newnode, range.item(0));
            for (var i=1; i < range.length; i++) {
                range.item(i).parentNode.removeChild(range[i]);
            };
            if (selectAfterPlace) {
                var range = this.document.getDocument().body.createTextRange();
                range.moveToElementText(newnode);
                range.select();
            };
        } else {
            var document = this.document.getDocument();
            var range = this.selection.createRange();

            range.pasteHTML('<img id="kupu-tempnode">');
            var tempnode = document.getElementById('kupu-tempnode');
            tempnode.replaceNode(newnode);

            if (selectAfterPlace) {
                // see MozillaSelection.replaceWithNode() for some comments about
                // selectAfterPlace
                if (selectAfterPlace.nodeType == Node.ELEMENT_NODE) {
                    range.moveToElementText(selectAfterPlace);
                } else {
                    range.moveToElementText(newnode);
                };
                range.select();
            };
        };
        this.reset();
        return newnode;
    };

    this.startOffset = function() {
        var startoffset = 0;
        var selrange = this.selection.createRange();
        var parent = selrange.parentElement();
        var elrange = selrange.duplicate();
        elrange.moveToElementText(parent);
        var tempstart = selrange.duplicate();
        while (elrange.compareEndPoints('StartToStart', tempstart) < 0) {
            startoffset++;
            tempstart.moveStart('character', -1);
        };

        return startoffset;
    };

    this.endOffset = function() {
        var endoffset = 0;
        var selrange = this.selection.createRange();
        var parent = selrange.parentElement();
        var elrange = selrange.duplicate();
        elrange.moveToElementText(parent);
        var tempend = selrange.duplicate();
        while (elrange.compareEndPoints('EndToEnd', tempend) > 0) {
            endoffset++;
            tempend.moveEnd('character', 1);
        };

        return endoffset;
    };

    this.getContentLength = function() {
        if (this.selection.type == 'Control') {
            return this.selection.createRange().length;
        };
        var contentlength = 0;
        var range = this.selection.createRange();
        var endrange = range.duplicate();
        while (range.compareEndPoints('StartToEnd', endrange) < 0) {
            range.move('character', 1);
            contentlength++;
        };
        return contentlength;
    };

    this.cutChunk = function(startOffset, endOffset) {
        /* cut a chunk of HTML from the selection

            this *should* return the chunk of HTML but doesn't yet
        */
        var range = this.selection.createRange().duplicate();
        range.moveStart('character', startOffset);
        range.moveEnd('character', -endOffset);
        range.pasteHTML('');
        // XXX here it should return the chunk
    };

    this.getElementLength = function(element) {
        /* returns the length of an element *including* 1 char for each child element

            this is defined on the selection since it returns results that can be used
            to work with selection offsets
        */
        var length = 0;
        var range = this.selection.createRange().duplicate();
        range.moveToElementText(element);
        range.moveStart('character', 1);
        range.moveEnd('character', -1);
        var endpoint = range.duplicate();
        endpoint.collapse(false);
        range.collapse();
        while (!range.isEqual(endpoint)) {
            range.moveEnd('character', 1);
            range.moveStart('character', 1);
            length++;
        };
        return length;
    };

    this.parentElement = function(allowmulti) {
        /* return the selected node (or the node containing the selection) */
        // XXX this should be on a range object
        if (this.selection.type == 'Control') {
            return this.selection.createRange().item(0);
        } else {
            return this.selection.createRange().parentElement();
        };
    };

    // deprecated alias of parentElement
    this.getSelectedNode = this.parentElement;

    this.moveStart = function(offset) {
        /* move the start of the selection */
        var range = this.selection.createRange();
        range.moveStart('character', offset);
        range.select();
        this.reset();
    };

    this.moveEnd = function(offset) {
        /* moves the end of the selection */
        var range = this.selection.createRange();
        range.moveEnd('character', offset);
        range.select();
        this.reset();
    };

    this.reset = function() {
       this.selection = this.document.getDocument().selection;
    };

    this.cloneContents = function() {
        /* returns a document fragment with a copy of the contents */
        var contents = this.selection.createRange().htmlText;
        var doc = this.document.getDocument();
        var docfrag = doc.createElement('span');
        docfrag.innerHTML = contents;
        return docfrag;
    };

    this.containsNode = function(node) {
        var selected = this.selection.createRange();
        
        if (this.selection.type.toLowerCase()=='text') {
            var range = doc.body.createTextRange();
            range.moveToElementText(node);

            if (selected.compareEndPoints('StartToEnd', range) >= 0 ||
                selected.compareEndPoints('EndToStart', range) <= 0) {
                return false;
            }
            return true;
        } else {
            for (var i = 0; i < selected.length; i++) {
                if (selected.item(i).contains(node)) {
                    return true;
                }
            }
            return false;
        }
    };
    
    this.getRange = function() {
        return this.selection.createRange();
    };

    this.restoreRange = function(range) {
        try {
            range.select();
            this.reset();
        } catch(e) {
        };
    };

    this.toString = function() {
        return this.selection.createRange().text;
    };

    this.intersectsNode = function(node) {
        var noderange = doc.body.createTextRange();
        noderange.moveToElementText(node);
        
        var selrange = this.selection.createRange();
        
        if((selrange.compareEndPoints('StartToStart', noderange) <= 0 &&
            selrange.compareEndPoints('EndToStart', noderange) > 0) ||
            (selrange.compareEndPoints('StartToStart', noderange) > 0 &&
            selrange.compareEndPoints('StartToEnd', noderange) < 0)) {
           return true;
        }
        return false;
    };
};

IESelection.prototype = new BaseSelection;

/* ContextFixer, fixes a problem with the prototype based model

    When a method is called in certain particular ways, for instance
    when it is used as an event handler, the context for the method
    is changed, so 'this' inside the method doesn't refer to the object
    on which the method is defined (or to which it is attached), but for
    instance to the element on which the method was bound to as an event
    handler. This class can be used to wrap such a method, the wrapper 
    has one method that can be used as the event handler instead. The
    constructor expects at least 2 arguments, first is a reference to the
    method, second the context (a reference to the object) and optionally
    it can cope with extra arguments, they will be passed to the method
    as arguments when it is called (which is a nice bonus of using 
    this wrapper).
*/

function ContextFixer(func, context) {
    /* Make sure 'this' inside a method points to its class */
    this.func = func;
    this.context = context;
    this.args = arguments;
    var self = this;
    
    this.execute = function() {
        /* execute the method */
        var args = [];
        // the first arguments will be the extra ones of the class
        for (var i=0; i < self.args.length - 2; i++) {
            args.push(self.args[i + 2]);
        };
        // the last are the ones passed on to the execute method
        for (var i=0; i < arguments.length; i++) {
            args.push(arguments[i]);
        };
        return self.func.apply(self.context, args);
    };

};

/* Alternative implementation of window.setTimeout

    This is a singleton class, the name of the single instance of the
    object is 'timer_instance', which has one public method called
    registerFunction. This method takes at least 2 arguments: a
    reference to the function (or method) to be called and the timeout.
    Arguments to the function are optional arguments to the 
    registerFunction method. Example:

    timer_instance.registerMethod(foo, 100, 'bar', 'baz');

    will call the function 'foo' with the arguments 'bar' and 'baz' with
    a timeout of 100 milliseconds.

    Since the method doesn't expect a string but a reference to a function
    and since it can handle arguments that are resolved within the current
    namespace rather then in the global namespace, the method can be used
    to call methods on objects from within the object (so this.foo calls
    this.foo instead of failing to find this inside the global namespace)
    and since the arguments aren't strings which are resolved in the global
    namespace the arguments work as expected even inside objects.

*/

function Timer() {
    /* class that has a method to replace window.setTimeout */
    this.lastid = 0;
    this.functions = {};
    
    this.registerFunction = function(object, func, timeout) {
        /* register a function to be called with a timeout

            args: 
                func - the function
                timeout - timeout in millisecs
                
            all other args will be passed 1:1 to the function when called
        */
        var args = [];
        for (var i=0; i < arguments.length - 3; i++) {
            args.push(arguments[i + 3]);
        }
        var id = this._createUniqueId();
        this.functions[id] = [object, func, args];
        setTimeout("timer_instance._handleFunction(" + id + ")", timeout);
    };

    this._handleFunction = function(id) {
        /* private method that does the actual function call */
        var obj = this.functions[id][0];
        var func = this.functions[id][1];
        var args = this.functions[id][2];
        this.functions[id] = null;
        func.apply(obj, args);
    };

    this._createUniqueId = function() {
        /* create a unique id to store the function by */
        while (this.lastid in this.functions && this.functions[this.lastid]) {
            this.lastid++;
            if (this.lastid > 100000) {
                this.lastid = 0;
            }
        }
        return this.lastid;
    };
};

// create a timer instance in the global namespace, obviously this does some
// polluting but I guess it's impossible to avoid...

// OBVIOUSLY THIS VARIABLE SHOULD NEVER BE OVERWRITTEN!!!
timer_instance = new Timer();

// helper function on the Array object to test for containment
Array.prototype.contains = function(element, objectequality) {
    /* see if some value is in this */
    for (var i=0; i < this.length; i++) {
        if (objectequality) {
            if (element === this[i]) {
                return true;
            };
        } else {
            if (element == this[i]) {
                return true;
            };
        };
    };
    return false;
};

// return a copy of an array with doubles removed
Array.prototype.removeDoubles = function() {
    var ret = [];
    for (var i=0; i < this.length; i++) {
        if (!ret.contains(this[i])) {
            ret.push(this[i]);
        };
    };
    return ret;
};

Array.prototype.map = function(func) {
    /* apply 'func' to each element in the array */
    for (var i=0; i < this.length; i++) {
        this[i] = func(this[i]);
    };
};

Array.prototype.reversed = function() {
    var ret = [];
    for (var i = this.length; i > 0; i--) {
        ret.push(this[i - 1]);
    };
    return ret;
};

// JavaScript has a friggin' blink() function, but not for string stripping...
String.prototype.strip = function() {
    var stripspace = /^\s*([\s\S]*?)\s*$/;
    return stripspace.exec(this)[1];
};

String.prototype.reduceWhitespace = function() {
    /* returns a string in which all whitespace is reduced 
    to a single, plain space */
    return this.replace(/\s+/g, ' ');
};
String.prototype.truncate = function(len) {
    if (this.length <= len) {
        return this;
    } else {
        var trimmed = this.substring(0, len+1).replace(/\s[^\s]*$/, '...');
        return trimmed;
    }
};

String.prototype.entitize = function() {
    var ret = this.replace(/&/g, '&amp;');
    ret = ret.replace(/"/g, '&quot;');
    ret = ret.replace(/'/g, '&apos;');
    ret = ret.replace(/</g, '&lt;');
    ret = ret.replace(/>/g, '&gt;');
    return ret;
};

String.prototype.deentitize = function() {
    var ret = this.replace(/&gt;/g, '>');
    ret = ret.replace(/&lt;/g, '<');
    ret = ret.replace(/&apos;/g, "'");
    ret = ret.replace(/&quot;/g, '"');
    ret = ret.replace(/&amp;/g, '&');
    return ret;
};

String.prototype.urldecode = function() {
    var reg = /%([a-fA-F0-9]{2})/g;
    var str = this;
    while (true) {
        var match = reg.exec(str);
        if (!match || !match.length) {
            break;
        };
        var repl = new RegExp(match[0], 'g');
        str = str.replace(repl, String.fromCharCode(parseInt(match[1], 16)));
    };
    return str;
};

String.prototype.centerTruncate = function(maxlength) {
    if (this.length <= maxlength) {
        return this;
    };
    var chunklength = maxlength / 2 - 3;
    var start = this.substr(0, chunklength);
    var end = this.substr(this.length - chunklength);
    return start + ' ... ' + end;
};

//----------------------------------------------------------------------------
// Exceptions
//----------------------------------------------------------------------------

function debug(str, win) {
    if (!win) {
        win = window;
    };
    var doc = win.document;
    var div = doc.createElement('div');
    div.appendChild(doc.createTextNode(str));
    doc.getElementsByTagName('body')[0].appendChild(div);
};

// XXX don't know if this is the regular way to define exceptions in JavaScript?
function Exception() {
    return;
};

// throw this as an exception inside an updateState handler to restart the
// update, may be required in situations where updateState changes the structure
// of the document (e.g. does a cleanup or so)
UpdateStateCancelBubble = new Exception();

function kupuFixImage(image) {
    image.removeAttribute('width');
    image.removeAttribute('height');
    var width = image.naturalWidth || image.width;
    var height = image.naturalHeight || image.height;
    if (height > width) {
        if (height > 128) {
            width = width * 128 / height;
            height = 128;
        };
    } else {
        if (width > 128) {
            height = height * 128 / width;
            width = 128;
        };
    };
    if (width&&height) {
        image.height = height;
        image.width = width;
    }
}

function toggleAltFieldVisibility(me) {
    var label = document.getElementById('image-alt-label');
    var vis = me.checked?'none':'';
    if (label) {
        label.style.display = vis;
        var fld = document.getElementById(label.htmlFor);
        if(fld) { fld.style.display = vis; }
    }
}

function getOuterHtml(node) {
    var html = '<';
    html += node.nodeName.toLowerCase();
    var attrs = node.attributes;
    for (var a = 0; a < attrs.length; a++) {
        var att = attrs[a];
        if (att.specified) {
            html += ' ' + att.nodeName.toLowerCase() + '="' + att.nodeValue + '"';
        }
    }
    html += '>';
    if (!(/hr|br|img|input/i.test(node.nodeName))) {
        html += node.innerHTML;
        html += '<\/' + node.nodeName.toLowerCase() + '>';
    }
    return html;
}


/* - kupueditor.js - */
/*****************************************************************************
 *
 * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/
// $Id: kupueditor.js 65353 2009-05-22 07:29:45Z duncan $

//----------------------------------------------------------------------------
// Main classes
//----------------------------------------------------------------------------

/* KupuDocument
    
    This essentially wraps the iframe.
    XXX Is this overkill?
    
*/

function KupuDocument(iframe) {
    /* Model */
    
    // attrs
    this.editable = iframe; // the iframe
    this.window = this.editable.contentWindow;
    this.document = this.window.document;

    this._browser = _SARISSA_IS_IE ? 'IE' : 'Mozilla';
    var DEPRECATED = { 'contentReadOnly': 'readonly', 'styleWithCSS': 'useCSS' };
    // methods
    this.execCommand = function(command, arg) {
        /* delegate execCommand */
        if (arg === undefined) arg = null;
        try {
            this.document.execCommand(command, false, arg);
        } catch(e) {
            command = DEPRECATED[command];
            if (command) {
                this.document.execCommand(command, false, !arg);
            };
        };
    };
    
    this.reloadSource = function() {
        /* reload the source */
        
        // XXX To temporarily work around problems with resetting the
        // state after a reload, currently the whole page is reloaded.
        // XXX Nasty workaround!! to solve refresh problems...
        document.location = document.location;
    };

    this.getDocument = function() {
        /* returns a reference to the window.document object of the iframe */
        return this.document;
    };

    this.getWindow = function() {
        /* returns a reference to the window object of the iframe */
        return this.window;
    };

    this.getSelection = function() {
        if (this._browser == 'Mozilla') {
            return new MozillaSelection(this);
        } else {
            return new IESelection(this);
        };
    };

    this.getEditable = function() {
        return this.editable;
    };
};

/* KupuEditor

    This controls the document, should be used from the UI.
    
*/

function KupuEditor(document, config, logger) {
    /* Controller */
    
    // attrs
    this.document = document; // the model
    this.config = config; // an object that holds the config values
    this.log = logger; // simple logger object
    this.tools = {}; // mapping id->tool
    this.filters = []; // contentfilters
    this.serializer = new XMLSerializer();
    
    this._designModeSetAttempts = 0;
    this._initialized = false;
    this._wantDesignMode = false;

    // some properties to save the selection, required for IE to remember 
    // where in the iframe the selection was
    this._previous_range = null;

    // this property is true if the content is changed, false if no changes 
    // are made yet
    this.content_changed = false;

    // methods
    this.initialize = function() {
        /* Should be called on iframe.onload, will initialize the editor */
        //DOM2Event.initRegistration();
        this._initializeEventHandlers();
        if (this.getBrowserName() == "IE") {
            var body = this.getInnerDocument().getElementsByTagName('body')[0];
            body.setAttribute('contentEditable', 'true');
            // provide an 'afterInit' method on KupuEditor.prototype
            // for additional bootstrapping (after editor init)
            this._initialized = true;
            if (this.afterInit) {
                this.afterInit();
            };
            this._saveSelection();
        } else {
            this._setDesignModeWhenReady();
        };
    };

    this.setContextMenu = function(menu) {
        /* initialize the contextmenu */
        menu.initialize(this);
    };

    this.registerTool = function(id, tool) {
        /* register a tool */
        this.tools[id] = tool;
        tool.initialize(this);
    };

    this.getTool = function(id) {
        /* get a tool by id */
        return this.tools[id];
    };

    this.registerFilter = function(filter) {
        /* register a content filter method

            the method will be called together with any other registered
            filters before the content is saved to the server, the methods
            can be used to filter any trash out of the content. they are
            called with 1 argument, which is a reference to the rootnode
            of the content tree (the html node)
        */
        this.filters.push(filter);
        filter.initialize(this);
    };

    this.updateStateHandler = function(event) {
        /* check whether the event is interesting enough to trigger the 
        updateState machinery and act accordingly */
        var interesting_codes = [8, 13, 37, 38, 39, 40, 46];
        // unfortunately it's not possible to do this on blur, since that's
        // too late. also (some versions of?) IE 5.5 doesn't support the
        // onbeforedeactivate event, which would be ideal here...
        this._saveSelection();

        if (event.type == 'click' ||
                (event.type == 'keyup' && 
                    interesting_codes.contains(event.keyCode))) {
            // Filthy trick to make the updateState method get called *after*
            // the event has been resolved. This way the updateState methods can
            // react to the situation *after* any actions have been performed (so
            // can actually stay up to date).
            this.updateState(event);
        }
    };
    
    this.updateState = function(event) {
        /* let each tool change state if required */
        // first see if the event is interesting enough to trigger
        // the whole updateState machinery
        var selNode = this.getSelectedNode();
        for (var id in this.tools) {
            try {
                this.tools[id].updateState(selNode, event);
            } catch (e) {
                if (e == UpdateStateCancelBubble) {
                    this.updateState(event);
                    break;
                } else {
                    this.logMessage(
                        'Exception while processing updateState on ' +
                            '${id}: ${msg}', {'id': id, 'msg': e}, 2);
                };
            };
        };
    };
    
    this.saveDocument = function(redirect, synchronous) {
        /* save the document

            the (optional) redirect argument can be used to make the client 
            jump to another URL when the save action was successful.

            synchronous is a boolean to allow sync saving (usually better to
            not save synchronous, since it may make browsers freeze on errors,
            this is used for saveOnPart, though)
        */
        
        // if no dst is available, bail out
        if (!this.config.dst) {
            this.logMessage(_('No destination URL available!'), 2);
            return;
        }
        var sourcetool = this.getTool('sourceedittool');
        if (sourcetool) {sourcetool.cancelSourceMode();};

        // make sure people can't edit or save during saving
        if (!this._initialized) {
            return;
        }
        this._initialized = false;
        
        // set the window status so people can see we're actually saving
        window.status= _("Please wait while saving document...");

        // call (optional) beforeSave() method on all tools
        for (var id in this.tools) {
            var tool = this.tools[id];
            if (tool.beforeSave) {
                try {
                    tool.beforeSave();
                } catch(e) {
                    alert(e);
                    this._initialized = true;
                    return;
                };
            };
        };
        
        // pass the content through the filters
        this.logMessage(_("Starting HTML cleanup"));
        var transform = this._filterContent(this.getInnerDocument().documentElement);

        // serialize to a string
        var contents = this._serializeOutputToString(transform);
        
        this.logMessage(_("Cleanup done, sending document to server"));
        var request = new XMLHttpRequest();
    
        if (!synchronous) {
            request.onreadystatechange = (new ContextFixer(this._saveCallback, 
                                               this, request, redirect)).execute;
            request.open("PUT", this.config.dst, true);
            request.setRequestHeader("Content-type", this.config.content_type);
            request.send(contents);
            this.logMessage(_("Request sent to server"));
        } else {
            this.logMessage(_('Sending request to server'));
            request.open("PUT", this.config.dst, false);
            request.setRequestHeader("Content-type", this.config.content_type);
            request.send(contents);
            this.handleSaveResponse(request,redirect);
        };
    };
    
    this.prepareForm = function(form, id) {
        /* add a field to the form and place the contents in it

            can be used for simple POST support where Kupu is part of a
            form
        */
        var sourcetool = this.getTool('sourceedittool');
        if (sourcetool) {sourcetool.cancelSourceMode();};

        // make sure people can't edit or save during saving
        if (!this._initialized) {
            return;
        }
        this._initialized = false;
        
        // set the window status so people can see we're actually saving
        window.status= _("Please wait while saving document...");

        // call (optional) beforeSave() method on all tools
        for (var tid in this.tools) {
            var tool = this.tools[tid];
            if (tool.beforeSave) {
                try {
                    tool.beforeSave();
                } catch(e) {
                    alert(e);
                    this._initialized = true;
                    return;
                };
            };
        };
        
        // set a default id
        if (!id) {
            id = 'kupu';
        };
        
        // pass the content through the filters
        this.logMessage(_("Starting HTML cleanup"));
        var transform = this._filterContent(this.getInnerDocument().documentElement);
        
        // XXX need to fix this.  Sometimes a spurious "\n\n" text 
        // node appears in the transform, which breaks the Moz 
        // serializer on .xml
        var contents =  this._serializeOutputToString(transform);
        
        this.logMessage(_("Cleanup done, sending document to server"));
        
        // now create the form input, since IE 5.5 doesn't support the 
        // ownerDocument property we use window.document as a fallback (which
        // will almost by definition be correct).
        var document = form.ownerDocument ? form.ownerDocument : window.document;
        var ta = document.createElement('textarea');
        ta.style.visibility = 'hidden';
        var text = document.createTextNode(contents);
        ta.appendChild(text);
        ta.setAttribute('name', id);
        
        // and add it to the form
        form.appendChild(ta);

        // let the calling code know we have added the textarea
        return true;
    };

    this.execCommand = function(command, param) {
        /* general stuff like making current selection bold, italics etc. 
            and adding basic elements such as lists
            */
        if (!this._initialized) {
            this.logMessage(_('Editor not initialized yet!'));
            return;
        };
        if (this.getBrowserName() == "IE") {
            this._restoreSelection();
        } else {
            this.focusDocument();
            if (command != 'styleWithCSS') {
                this.content_changed = true;
                // Done here otherwise it doesn't always work or gets lost
                // after some commands
                this.getDocument().execCommand('styleWithCSS', false);
            };
        };
        this.getDocument().execCommand(command, param);
        this.updateState();
    };

    this.getSelection = function() {
        /* returns a Selection object wrapping the current selection */
        this._restoreSelection();
        return this.getDocument().getSelection();
    };

    this.getSelectedNode = function(allowmulti) {
        /* returns the selected node (read: parent) or none */
        /* if allowmulti is true, returns the parent of all ranges in the
           selection (in the rare case that selection has more than one
           range) */
        return this.getSelection().parentElement(allowmulti);
    };

    this.getNearestParentOfType = function(node, type) {
        /* well the title says it all ;) */
        var type = type.toLowerCase();
        while (node) {
            if (node.nodeName.toLowerCase() == type) {
                return node;
            }   
            var node = node.parentNode;
        }
        return false;
    };

    this.removeNearestParentOfType = function(node, type) {
        var nearest = this.getNearestParentOfType(node, type);
        if (!nearest) {
            return false;
        };
        var parent = nearest.parentNode;
        while (nearest.childNodes.length) {
            var child = nearest.firstChild;
            child = nearest.removeChild(child);
            parent.insertBefore(child, nearest);
        };
        parent.removeChild(nearest);
    };

    this.getDocument = function() {
        /* returns a reference to the document object that wraps the iframe */
        return this.document;
    };

    this.getInnerDocument = function() {
        /* returns a reference to the window.document object of the iframe */
        return this.getDocument().getDocument();
    };

    this.insertNodeAtSelection = function(insertNode, selectNode) {
        /* insert a newly created node into the document */
        if (!this._initialized) {
            this.logMessage(_('Editor not initialized yet!'));
            return;
        };

        this.content_changed = true;

        var browser = this.getBrowserName();
        if (browser != "IE") {
            this.focusDocument();
        };
        
        var ret = this.getSelection().replaceWithNode(insertNode, selectNode);
        this._saveSelection();

        return ret;
    };

    this.focusDocument = function() {
        this.getDocument().getWindow().focus();
    };

    this.logMessage = function(message, severity) {
        /* log a message using the logger, severity can be 0 (message, default), 1 (warning) or 2 (error) */
        this.log.log(message, severity);
    };

    this.registerContentChanger = function(element) {
        /* set this.content_changed to true (marking the content changed) when the 
            element's onchange is called
        */
        addEventHandler(element, 'change', function() {this.content_changed = true;}, this);
    };
    
    // helper methods
    this.getBrowserName = function() {
        /* returns either 'Mozilla' (for Mozilla, Firebird, Netscape etc.) or 'IE' */
        if (_SARISSA_IS_MOZ) {
            return "Mozilla";
        } else if (_SARISSA_IS_IE) {
            return "IE";
        } else {
            throw _("Browser not supported!");
        }
    };
    
    this.handleSaveResponse = function(request, redirect) {
        // mind the 1223 status, somehow IE gives that sometimes (on 204?)
        // at first we didn't want to add it here, since it's a specific IE
        // bug, but too many users had trouble with it...
        if (request.status != '200' && request.status != '204' &&
                request.status != '1223') {
            var msg = _('Error saving your data.\nResponse status: ' + 
                            '${status}.\nCheck your server log for more ' +
                            'information.', {'status': request.status});
            alert(msg);
            window.status = _("Error saving document");
        } else if (redirect) { // && (!request.status || request.status == '200' || request.status == '204'))
            window.document.location = redirect;
            this.content_changed = false;
        } else {
            // clear content_changed before reloadSrc so saveOnPart is not triggered
            this.content_changed = false;
            if (this.config.reload_after_save) {
                this.reloadSrc();
            };
            // we're done so we can start editing again
            window.status= _("Document saved");
        };
        this._initialized = true;
    };

    // private methods
    this._addEventHandler = addEventHandler;

    this._saveCallback = function(request, redirect) {
        /* callback for Sarissa */
        if (request.readyState == 4) {
            this.handleSaveResponse(request, redirect);
        };
    };
    
    this.reloadSrc = function() {
        /* reload the src, called after a save when reload_src is set to true */
        // XXX Broken!!!
        /*
        if (this.getBrowserName() == "Mozilla") {
            this.getInnerDocument().designMode = "Off";
        }
        */
        // XXX call reloadSrc() which has a workaround, reloads the full page
        // instead of just the iframe...
        this.getDocument().reloadSource();
        if (this.getBrowserName() == "Mozilla") {
            this.getInnerDocument().designMode = "On";
        };
        /*
        var selNode = this.getSelectedNode();
        this.updateState(selNode);
        */
    };

    // Fixup Mozilla breaking image src url when dragging images
    this.imageInserted = function(event) {
        var node = event.target;
        if (node && node.nodeType==1) {
            var nodes = (/^img$/i.test(node.nodeName))?[node]:node.getElementsByTagName('img');
            for (var i = 0; i < nodes.length; i++) {
                node = nodes[i];
                var src = node.getAttribute('kupu-src');
                if (src) { node.src = src; };
            };
        };
    };
    // Prevent Mozilla resizing of images
    this.imageModified = function(event) {
        var node = event.target;
        if (node && (/^img$/i.test(node.nodeName))) {
            if (event.attrName=="style" && event.attrChange==1 && (/height|width/.test(event.newValue))) {
                timer_instance.registerFunction(this, this._clearStyle, 1, node);
            }
        };
    };
    // Make sure image size is set on width/height attributes not style.
    this._clearStyle = function(node) {
        var w = node.width;
        var h = node.height;
        node.style.width = "";
        node.style.height = "";
        if (this.okresize) {
            if (w) {node.width = w;};
            if (h) {node.height = h;};
        };
    };
    this._cancelResize = function(evt) {
        return false;
    };

    this._initializeEventHandlers = function() {
        /* attache the event handlers to the iframe */
        var win = this.getDocument().getWindow();
        var idoc = this.getInnerDocument();
        var e = this._addEventHandler;
        var validattrs =  this.xhtmlvalid.tagAttributes.img;
        this.okresize = validattrs.contains('width') && validattrs.contains('height');
        // Set design mode on resize event:
        e(win, 'resize', this._resizeHandler, this);
        // Initialize DOM2Event compatibility
        // XXX should come back and change to passing in an element
        e(idoc, "click", this.updateStateHandler, this);
        e(idoc, "dblclick", this.updateStateHandler, this);
        e(idoc, "keyup", this.updateStateHandler, this);
        e(idoc, "keyup", function() {this.content_changed = true;}, this);
        e(idoc, "mouseup", this.updateStateHandler, this);
        if (this.getBrowserName() == "IE") {
            e(idoc, "selectionchange", this.onSelectionChange, this);
            if (!this.okresize) { e(idoc.documentElement, "resizestart", this._cancelResize, this);};
        } else {
            e(idoc, "DOMNodeInserted", this.imageInserted, this);
            e(idoc, "DOMAttrModified", this.imageModified, this);
        }
    };

    this._resizeHandler = function() {
        // Use the resize event to trigger setting design mode
        if (this._wantDesignMode) {
            this._setDesignModeWhenReady();
        }
    };
    
    this._setDesignModeWhenReady = function() {
        /* Try to set design mode, but if we fail then just wait for a
         * resize event.
         */
        var success = false;
        try {
            this._setDesignMode();
            success = true;
        } catch (e) {
        };
        if (success) {
            this._wantDesignMode = false;
            // provide an 'afterInit' method on KupuEditor.prototype
            // for additional bootstrapping (after editor init)
            if (this.afterInit) {
                this.afterInit();
            };
        } else {
            this._wantDesignMode = true; // Enable the resize trigger
        }
    };

    this._setDesignMode = function() {
        this.getInnerDocument().designMode = "On";
        this.execCommand("undo");
        // note the negation: the argument doesn't work as expected...
        this._initialized = true;
    };

    this._saveSelection = function() {
        /* Save the selection, works around a problem with IE where the 
         selection in the iframe gets lost. We only save if the current 
         selection in the document */
        if (this._isDocumentSelected()) {
            var cursel = this.getInnerDocument().selection;
            var currange = cursel.createRange();
            if (cursel.type=="Control" && currange.item(0).nodeName.toLowerCase()=="body") {
                /* This happens when you try to active an embedded
                 * object */
                this._restoreSelection(true);
                return;
            }
            this._previous_range = currange;
        };
    };

    this._restoreSelection = function(force) {
        /* re-selects the previous selection in IE. We only restore if the
        current selection is not in the document.*/
        if (this._previous_range && (force || !this._isDocumentSelected())) {
            try {
                this._previous_range.select();
            } catch (e) { };
        };
    };
    
    if (this.getBrowserName() != "IE") {
        this._saveSelection = function() {};
        this._restoreSelection = function() {};
    }

    this.onSelectionChange = function(event) {
        this._saveSelection();
    };

    this._isDocumentSelected = function() {
        if (this.suspended) return false;

        var editable_body = this.getInnerDocument().getElementsByTagName('body')[0];
        try {
            var selrange = this.getInnerDocument().selection.createRange();
        } catch(e) {
            return false;
        }
        var someelement = selrange.parentElement ? selrange.parentElement() : selrange.item(0);

        while (someelement.nodeName.toLowerCase() != 'body') {
            someelement = someelement.parentNode;
        };
        
        return someelement == editable_body;
    };

    this._clearSelection = function() {
        /* clear the last stored selection */
        this._previous_range = null;
    };

    this._filterContent = function(documentElement) {            
        /* pass the content through all the filters */
        // first copy all nodes to a Sarissa document so it's usable
        var xhtmldoc = Sarissa.getDomDocument();
        var doc = this._convertToSarissaNode(xhtmldoc, documentElement);
        // now pass it through all filters
        for (var i=0; i < this.filters.length; i++) {
            var doc = this.filters[i].filter(xhtmldoc, doc);
        };
        // fix some possible structural problems, such as an empty or missing head, title
        // or script or textarea tags without closing tag...
        this._fixXML(doc, xhtmldoc);
        return doc;
    };

    this.getXMLBody = function(transform) {
        var bodies = transform.getElementsByTagName('body');
        var data = '';
        for (var i = 0; i < bodies.length; i++) {
            data += this.serializer.serializeToString(bodies[i]);
        }
        return this.layoutsource(this.escapeEntities(data));
    };

    this.getHTMLBody = function() {
        var doc = this.getInnerDocument();
        var docel = doc.documentElement;
        var bodies = docel.getElementsByTagName('body');
        var data = '';
        for (var i = 0; i < bodies.length; i++) {
            data += bodies[i].innerHTML;
        }
        return this.layoutsource(this.escapeEntities(data));
    };

    // If we have multiple bodies this needs to remove the extras.
    this.setHTMLBody = function(text) {
        var doc = this.getInnerDocument().documentElement;
        var bodies = doc.getElementsByTagName('body');
        for (var i = 0; i < bodies.length-1; i++) {
            bodies[i].parentNode.removeChild(bodies[i]);
        }
        if (_SARISSA_IS_IE) { /* IE converts certain comments to visible text so strip them */
            text = text.replace(/<!--\[.*?-->/g, '');

        } else { /* Mozilla doesn't understand strong/em */
            var fixups = { 'strong':'b', 'em':'i' };

            text = text.replace(/<(\/?)(strong|em)\b([^>]*)>/gi, function(all,close,tag,attrs) {
                tag = fixups[tag.toLowerCase()];
                return '<'+close+tag+attrs+'>';
            });
        };
        text = text.replace(/<p>(<hr.*?>)<\/p>/g,'$1');
        bodies[bodies.length-1].innerHTML = text;
        /* Mozilla corrupts dragged images, so save the src attribute */
        var nodes = doc.getElementsByTagName('img');
        for (var i = 0; i < nodes.length; i++) {
            var node = nodes[i];
            node.setAttribute('kupu-src', node.src);
        };
    };

    this._fixXML = function(doc, document) {
        /* fix some structural problems in the XML that make it invalid XTHML */
        // find if we have a head and title, and if not add them
        var heads = doc.getElementsByTagName('head');
        var titles = doc.getElementsByTagName('title');
        if (!heads.length) {
            // assume we have a body, guess Kupu won't work without one anyway ;)
            var body = doc.getElementsByTagName('body')[0];
            var head = document.createElement('head');
            body.parentNode.insertBefore(head, body);
            var title = document.createElement('title');
            var titletext = document.createTextNode('');
            head.appendChild(title);
            title.appendChild(titletext);
        } else if (!titles.length) {
            var head = heads[0];
            var title = document.createElement('title');
            var titletext = document.createTextNode('');
            head.appendChild(title);
            title.appendChild(titletext);
        };
        // create a closing element for all elements that require one in XHTML
        var dualtons = ['a', 'abbr', 'acronym', 'address', 'applet', 
            'b', 'bdo', 'big', 'blink', 'blockquote', 
            'button', 'caption', 'center', 'cite', 
            'comment', 'del', 'dfn', 'dir', 'div',
            'dl', 'dt', 'em', 'embed', 'fieldset',
            'font', 'form', 'frameset', 'h1', 'h2',
            'h3', 'h4', 'h5', 'h6', 'i', 'iframe',
            'ins', 'kbd', 'label', 'legend', 'li',
            'listing', 'map', 'marquee', 'menu',
            'multicol', 'nobr', 'noembed', 'noframes',
            'noscript', 'object', 'ol', 'optgroup',
            'option', 'p', 'pre', 'q', 's', 'script',
            'select', 'small', 'span', 'strike', 
            'strong', 'style', 'sub', 'sup', 'table',
            'tbody', 'td', 'textarea', 'tfoot',
            'th', 'thead', 'title', 'tr', 'tt', 'u',
            'ul', 'xmp'];
        // XXX I reckon this is *way* slow, can we use XPath instead or
        // something to speed this up?
        for (var i=0; i < dualtons.length; i++) {
            var elname = dualtons[i];
            var els = doc.getElementsByTagName(elname);
            for (var j=0; j < els.length; j++) {
                var el = els[j];
                if (!el.hasChildNodes()) {
                    var child = document.createTextNode('');
                    el.appendChild(child);
                };
            };
        };
    };

    this.xhtmlvalid = new XhtmlValidation(this);

    this._convertToSarissaNode = function(ownerdoc, htmlnode) {
        /* Given a string of non-well-formed HTML, return a string of 
           well-formed XHTML.

           This function works by leveraging the already-excellent HTML 
           parser inside the browser, which generally can turn a pile 
           of crap into a DOM.  We iterate over the HTML DOM, appending 
           new nodes (elements and attributes) into a node.

           The primary problems this tries to solve for crappy HTML: mixed 
           element names, elements that open but don't close, 
           and attributes that aren't in quotes.  This can also be adapted 
           to filter out tags that you don't want and clean up inline styles.

           Inspired by Guido, adapted by Paul from something in usenet.
           Tag and attribute tables added by Duncan
        */
        return this.xhtmlvalid._convertToSarissaNode(ownerdoc, htmlnode);
    };

    this._fixupSingletons = function(xml) {
        return xml.replace(/<([^>]+)\/>/g, "<$1 />");
    };
    this._serializeOutputToString = function(transform) {
        // XXX need to fix this.  Sometimes a spurious "\n\n" text 
        // node appears in the transform, which breaks the Moz 
        // serializer on .xml
            
        if (this.config.strict_output) {
            var contents =  '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ' + 
                            '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n' + 
                            '<html xmlns="http://www.w3.org/1999/xhtml">' +
                            this.serializer.serializeToString(transform.getElementsByTagName("head")[0]) +
                            this.serializer.serializeToString(transform.getElementsByTagName("body")[0]) +
                            '</html>';
        } else {
            var contents = '<html>' + 
                            this.serializer.serializeToString(transform.getElementsByTagName("head")[0]) +
                            this.serializer.serializeToString(transform.getElementsByTagName("body")[0]) +
                            '</html>';
        };

        contents = this.escapeEntities(contents);

        if (this.config.compatible_singletons) {
            contents = this._fixupSingletons(contents);
        };
        
        return contents;
    };
    this.layoutsource = function(data) {
        data = data.replace(
            /\s*(<(p|div|h.|ul|ol|dl|menu|dir|pre|blockquote|address|center|table|thead|tbody|tfoot|tr|th|td))\b/ig, '\n$1');
        data = data.replace(
            /\s*(<\/(p|div|h.|ul|ol|dl|menu|dir|pre|blockquote|address|center|table|thead|tbody|tfoot|tr|th|td)>)\s*/ig, '$1\n');
        data = data.replace(/\<pre\>((?:.|\n)*?)\<\/pre\>/gm, function(s) {
            return s.replace(/<br\b[^>]*>/gi,'\n');
            });
        return data.strip();
    };
    this.escapeEntities = function(xml) {
        // XXX: temporarily disabled
        xml = xml.replace(/\xa0/g, '&nbsp;');
        return xml;
        // Escape non-ascii characters as entities.
//         return xml.replace(/[^\r\n -\177]/g,
//             function(c) {
//             return '&#'+c.charCodeAt(0)+';';
//         });
    };

    this.getFullEditor = function() {
        var fulleditor = this.getDocument().getEditable();
        while (!(/kupu-fulleditor/.test(fulleditor.className))) {
            fulleditor = fulleditor.parentNode;
        }
        return fulleditor;
    };
    // Control the className and hence the style for the whole editor.
    this.setClass = function(name) {
        this.getFullEditor().className += ' '+name;
    };
    
    this.clearClass = function(name) {
        var fulleditor = this.getFullEditor();
        fulleditor.className = fulleditor.className.replace(' '+name, '');
    };

    var busycount = 0;
    this.busy = function() {
        if (busycount <= 0) {
            this.setClass('kupu-busy');
        }
        busycount++;
    };
    this.notbusy = function(force) {
        busycount = force?0:busycount?busycount-1:0;
        if (busycount <= 0) {
            this.clearClass('kupu-busy');
        }
    };

    this.suspendEditing = function() {
        this._previous_range = this.getSelection().getRange();
        this.setClass('kupu-modal');
        for (var id in this.tools) {
            this.tools[id].disable();
        }
        if (this.getBrowserName() == "IE") {
            var body = this.getInnerDocument().getElementsByTagName('body')[0];
            body.setAttribute('contentEditable', 'false');
        } else {
            this.getDocument().execCommand('contentReadOnly', 'true');
        }
        this.suspended = true;
    };
    
    this.resumeEditing = function() {
        if (!this.suspended) {
            return;
        }
        this.clearClass('kupu-modal');
        for (var id in this.tools) {
            this.tools[id].enable();
        }
        if (this.getBrowserName() == "IE") {
            var body = this.getInnerDocument().getElementsByTagName('body')[0];
            body.setAttribute('contentEditable', 'true');
            this._restoreSelection();
        } else {
            var doc = this.getInnerDocument();
            this.getDocument().execCommand('contentReadOnly', 'false');
            doc.designMode = "On";
            this.focusDocument();
            this.getSelection().restoreRange(this._previous_range);
        }
        this.suspended = false;
    };
    this.newElement = function(tagName) {
        return newDocumentElement(this.getInnerDocument(), tagName, arguments);
    };
    this.newText = function(text) {
        return this.getInnerDocument().createTextNode(text);
    };
}




/* - kupubasetools.js - */
/*****************************************************************************
 *
 * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/
// $Id: kupubasetools.js 59216 2008-10-18 15:50:04Z duncan $

//----------------------------------------------------------------------------
//
// Toolboxes
//
//  These are addons for Kupu, simple plugins that implement a certain 
//  interface to provide functionality and control view aspects.
//
//----------------------------------------------------------------------------

//----------------------------------------------------------------------------
// Superclasses
//----------------------------------------------------------------------------

function KupuTool() {
    /* Superclass (or actually more of an interface) for tools 
    
        Tools must implement at least an initialize method and an 
        updateState method, and can implement other methods to add 
        certain extra functionality (e.g. createContextMenuElements).
    */

    this.toolboxes = {};

    // private methods
    addEventHandler = addEventHandler;
};

// methods
KupuTool.prototype.initialize = function(editor) {
    /* Initialize the tool.

        Obviously this can be overriden but it will do
        for the most simple cases
    */
    this.editor = editor;
};

KupuTool.prototype.registerToolBox = function(id, toolbox) {
    /* register a ui box 
    
        Note that this needs to be called *after* the tool has been 
        registered to the KupuEditor
    */
    this.toolboxes[id] = toolbox;
    toolbox.initialize(this, this.editor);
};

KupuTool.prototype.updateState = function(selNode, event) {
    /* Is called when user moves cursor to other element 

        Calls the updateState for all toolboxes and may want perform
        some actions itself
    */
    for (var id in this.toolboxes) {
        this.toolboxes[id].updateState(selNode, event);
    };
};

KupuTool.prototype.enable = function() {
    // Called when the tool is enabled after a form is dismissed.
};

KupuTool.prototype.disable = function() {
    // Called when the tool is disabled (e.g. for a modal form)
};

function KupuToolBox() {
    /* Superclass for a user-interface object that controls a tool */
};

KupuToolBox.prototype.initialize = function(tool, editor) {
    /* store a reference to the tool and the editor */
    this.tool = tool;
    this.editor = editor;
};

KupuToolBox.prototype.updateState = function(selNode, event) {
    /* update the toolbox according to the current iframe's situation */
};

function noContextMenu(object) {
    /* Decorator for a tool to suppress the context menu */
    object.createContextMenuElements = function(selNode, event) {
        return [];
    };
    return object;
}

// Helper function for enabling/disabling tools
function kupuButtonDisable(button) {
    button = button || this.button;
    if (button) {
        button.disabled = "disabled";
        button.className += ' disabled';
    }
};

function kupuButtonEnable(button) {
    button = button || this.button;
    if (button) {
        button.disabled = "";
        button.className = button.className.replace(/ *\bdisabled\b/g, '');
    }
};

//----------------------------------------------------------------------------
// Implementations
//----------------------------------------------------------------------------

function KupuButton(buttonid, commandfunc, tool) {
    /* Base prototype for kupu button tools */
    if (arguments.length) {
        this.buttonid = buttonid;
        this.button = getFromSelector(buttonid);
        this.commandfunc = commandfunc;
        this.tool = tool;
        this.disable = kupuButtonDisable;
        this.enable = kupuButtonEnable;
    };
};

KupuButton.prototype = new KupuTool;

KupuButton.prototype.initialize = function(editor) {
    this.editor = editor;
    if (!this.button) return;
    addEventHandler(this.button, 'click', this.execCommand, this);
};

KupuButton.prototype.execCommand = function() {
    /* exec this button's command */
    this.commandfunc(this, this.editor, this.tool);
};

KupuButton.prototype.updateState = function(selNode, event) {
    /* override this in subclasses to determine whether a button should
        look 'pressed in' or not
    */
};

function KupuStateButton(buttonid, commandfunc, checkfunc, offclass, onclass) {
    /* A button that can have two states (e.g. pressed and
       not-pressed) based on CSS classes */
    this.buttonid = buttonid;
    this.button = getFromSelector(buttonid);
    this.commandfunc = commandfunc;
    this.checkfunc = checkfunc;
    this.offclass = offclass;
    this.onclass = onclass;
    this.pressed = false;

    this.execCommand = function() {
        /* exec this button's command */
        this.button.className = (this.pressed ? this.offclass : this.onclass);
        this.pressed = !this.pressed;
        this.editor.focusDocument();
        this.commandfunc(this, this.editor);
    };

    this.updateState = function(selNode, event) {
        /* check if we need to be clicked or unclicked, and update accordingly 
        
            if the state of the button should be changed, we set the class
        */
        if (!this.button) return;
        var currclass = this.button.className;
        var newclass = null;
        if (this.checkfunc(selNode, this, this.editor, event)) {
            newclass = this.onclass;
            this.pressed = true;
        } else {
            newclass = this.offclass;
            this.pressed = false;
        };
        if (currclass != newclass) {
            this.button.className = newclass;
        };
    };
};

KupuStateButton.prototype = new KupuButton;

/* Same as the state button, but the focusDocument call is delayed.
 * Mozilla&Firefox have a bug on windows which can cause a crash if you
 * change CSS positioning styles on an element which has focus.
 */
function KupuLateFocusStateButton(buttonid, commandfunc, checkfunc,
        offclass, onclass) {
    KupuStateButton.apply(this, [buttonid, commandfunc, checkfunc,
                                 offclass, onclass]);
}

KupuLateFocusStateButton.prototype = new KupuStateButton;

KupuLateFocusStateButton.prototype.execCommand = function() {
    /* exec this button's command */
    this.button.className = (this.pressed ? this.offclass : this.onclass);
    this.pressed = !this.pressed;
    this.commandfunc(this, this.editor);
    this.editor.focusDocument();
};

function KupuRemoveElementButton(buttonid, element_name, cssclass) {
    /* A button specialized in removing elements in the current node
       context. Typical usages include removing links, images, etc. */
    this.button = getFromSelector(buttonid);
    this.element_name = element_name;
    this.onclass = 'invisible';
    this.offclass = cssclass;
    this.pressed = false;
};

KupuRemoveElementButton.prototype = new KupuStateButton;

KupuRemoveElementButton.prototype.commandfunc = function(button, editor) {
    editor.focusDocument();
    editor.removeNearestParentOfType(editor.getSelectedNode(), this.element_name);
    editor.updateState();
};

KupuRemoveElementButton.prototype.checkfunc = function(currnode, button,
        editor, event) {
    var element = editor.getNearestParentOfType(currnode, this.element_name);
    return (element ? false : true);
};

function KupuUI(textstyleselectid) {
    /* View 
    
        This is the main view, which controls most of the toolbar buttons.
        Even though this is probably never going to be removed from the view,
        it was easier to implement this as a plain tool (plugin) as well.
    */
    
    // attributes
    this.tsselect = getFromSelector(textstyleselectid);
    this.paraoptions = [];
    this.tableoptions = [];
    this.styleoptions = [];
    this.tableoffset = 0;
    this.styleoffset = 0;
    this.tablegrp = null;
    this.optionstate = -1;
    this.otherstyle = null;
    this.tablestyles = {};
    this.charstyles = {};
    this.styles = {}; // use an object here so we can use the 'in' operator later on
    this.blocktagre = /^(p|div|h.|ul|ol|dl|menu|dir|pre|blockquote|address|center)$/i;
    this.spanre = /^span\b/i;
    this.tblre = /^thead|tbody|table|t[rdh]\b/i;
};


KupuUI.prototype = new KupuTool;

KupuUI.prototype.initialize = function(editor) {
    /* initialize the ui like tools */
    this.editor = editor;
    this.cleanStyles();
    this.enableOptions(false);
    if (this.tsselect) {
        this._selectevent = addEventHandler(this.tsselect, 'change', this.setTextStyleHandler, this);
    }
};

KupuUI.prototype.getStyles = function() {
    if (!this.paraoptions) {
        this.cleanStyles();
    }
    return [ this.paraoptions, this.tableoptions ];
};

KupuUI.prototype.setTextStyleHandler = function(event) {
    this.setTextStyle(this.tsselect.options[this.tsselect.selectedIndex].value);
};

// event handlers
KupuUI.prototype.basicButtonHandler = function(action) {
    /* event handler for basic actions (toolbar buttons) */
    this.editor.execCommand(action);
    this.editor.updateState();
};

KupuUI.prototype.saveButtonHandler = function() {
    /* handler for the save button */
    this.editor.saveDocument();
};

KupuUI.prototype.saveAndExitButtonHandler = function(redirect_url) {
    /* save the document and, if successful, redirect */
    this.editor.saveDocument(redirect_url);
};

KupuUI.prototype.cutButtonHandler = function() {
    try {
        this.editor.execCommand('Cut');
    } catch (e) {
        if (this.editor.getBrowserName() == 'Mozilla') {
            alert(_('Cutting from JavaScript is disabled on your Mozilla due to security settings. For more information, read http://www.mozilla.org/editor/midasdemo/securityprefs.html'));
        } else {
            throw e;
        };
    };
    this.editor.updateState();
};

KupuUI.prototype.copyButtonHandler = function() {
    try {
        this.editor.execCommand('Copy');
    } catch (e) {
        if (this.editor.getBrowserName() == 'Mozilla') {
            alert(_('Copying from JavaScript is disabled on your Mozilla due to security settings. For more information, read http://www.mozilla.org/editor/midasdemo/securityprefs.html'));
        } else {
            throw e;
        };
    };
    this.editor.updateState();
};

KupuUI.prototype.pasteButtonHandler = function() {
    try {
        this.editor.execCommand('Paste');
    } catch (e) {
        if (this.editor.getBrowserName() == 'Mozilla') {
            alert(_('Pasting from JavaScript is disabled on your Mozilla due to security settings. For more information, read http://www.mozilla.org/editor/midasdemo/securityprefs.html'));
        } else {
            throw e;
        };
    };
    this.editor.updateState();
};

KupuUI.prototype.cleanStyles = function() {
    if (!this.tsselect) return;
    var options = this.tsselect.options;
    var parastyles = this.styles;
    var tablestyles = this.tablestyles;
    var charstyles = this.charstyles;
    
    var normal = ['Normal', 'p|'];
    var td = ['Plain Cell', 'td|'];
    var nostyle = ['(remove style)', ''];

    var opts = [];
    while (options.length) {
        var opt = options[0];
        options[0] = null;
        var v = opt.value;
        if (v.indexOf('|') > -1) {
            var split = v.split('|');
            v = split[0].toLowerCase() + "|" + split[1];
        } else {
            v = v.toLowerCase()+"|";
        };
        var optarray = [opt.text, v];
        if (v=='td|') {
            td = optarray;
        } else if (v=='p|') {
            normal = optarray;
        } else if (v=='') {
            nostyle = optarray;
        } else {
            opts.push([opt.text,v]);
        }
    }
    this.tableoptions.push(td);
    tablestyles[td[1]] = 0;
    this.paraoptions.push(normal);
    parastyles[normal[1]] = 0;

    for (var i = 0; i < opts.length; i++) {
        optarray = opts[i];
        v = optarray[1];

        if (this.spanre.test(v)) {
            charstyles[v] = this.styleoptions.length;
            this.styleoptions.push(optarray);
        } else if (this.tblre.test(v)) {
            tablestyles[v] = this.tableoptions.length;
            this.tableoptions.push(optarray);
        } else {
            parastyles[v] = this.paraoptions.length;
            this.paraoptions.push(optarray);
        };
    };
    this.paraoptions.push(nostyle);
    this.styleoffset = this.paraoptions.length;
    this.tableoffset = this.styleoffset + this.styleoptions.length;
};

// Remove otherstyle and switch to appropriate style set.
KupuUI.prototype.enableOptions = function(inTable) {
    if (!this.tsselect) return;
    var select = this.tsselect;
    var options = select.options;
    if (this.otherstyle) {
        options[0] = null;
        this.otherstyle = null;
    }
    if (this.optionstate == inTable) return; /* No change */

    // while (select.firstChild) select.removeChild(select.firstChild);

    function option(info) {
        return newElement('option', {'value': info[1]}, [info[0]]);
    }
    if (this.optionstate==-1) {
        for (var i = 0; i < this.paraoptions.length; i++) {
            select.appendChild(option(this.paraoptions[i]));
        }
        if (this.styleoptions.length) {
            var grp = document.createElement('optgroup');
            grp.label = 'Character styles';
            for (var i = 0; i < this.styleoptions.length; i++) {
                grp.appendChild(option(this.styleoptions[i]));
            }
            select.appendChild(grp);
        }
    }
    if (inTable) {
        var grp = (this.tablegrp = document.createElement('optgroup'));
        grp.label = 'Table elements';
        for (var i = 0; i < this.tableoptions.length; i++) {
            grp.appendChild(option(this.tableoptions[i]));
        }
        select.appendChild(grp);
    } else {
        while (select.options[this.tableoffset]) {
            select.options[this.tableoffset] = null;
        };
        if (this.tablegrp) {
            select.removeChild(this.tablegrp);
            this.tablegrp = null;
        };
    };
    this.optionstate = inTable;
};

KupuUI.prototype.setIndex = function(currnode, tag, index, styles) {
    var className = currnode.className;
    this.styletag = tag;
    this.classname = className;
    var style = tag+'|'+className;

    if (style in styles) {
        return styles[style];
    } else if (!className && tag in styles) {
        return styles[tag];
    }
    return index;
};

KupuUI.prototype.nodeStyle = function(node) {
    var currnode = node;
    var index = -1;
    this.styletag = undefined;
    this.classname = '';

    // Set the table state correctly
    this.intable = false;

    while(currnode) {
        var tag = currnode.nodeName;
        if (/^body$/i.test(tag)) break;
        if (this.tblre.test(tag)) {
            this.intable = true;
            break;
        };
        currnode = currnode.parentNode;
    };
    currnode = node;
    while (currnode) {
        var tag = currnode.nodeName.toLowerCase();

        if (/^body$/.test(tag)) {
            if (!this.styletag) {
                // Forced style messes up in Firefox: return -1 to
                // indicate no style 
                return -1;
            }
            break;
        }
        if (this.spanre.test(tag)) {
            index = this.setIndex(currnode, tag, index, this.charstyles);
            if (index >= 0) return index+this.styleoffset; // span takes priority
        } else if (this.blocktagre.test(tag)) {
            index = this.setIndex(currnode, tag, index, this.styles);
        } else if (this.tblre.test(tag)) {
            if (index > 0) return index; // block or span takes priority.
            index = this.setIndex(currnode, tag, index, this.tablestyles);
            if (index >= 0 || tag=='table') {
                return index+this.tableoffset; // Stop processing if in a table
            }
        }
        currnode = currnode.parentNode;
    }
    return index;
};

KupuUI.prototype.updateState = function(selNode) {
    /* set the text-style pulldown */

    // first get the nearest style
    // search the list of nodes like in the original one, break if we encounter a match,
    // this method does some more than the original one since it can handle commands in
    // the form of '<style>|<classname>' next to the plain
    // '<style>' commands
    if (!this.tsselect) return;
    var index = undefined;
    var mixed = false;
    var styletag, classname;

    var selection = this.editor.getSelection();

    for (var el=selNode.firstChild; el; el=el.nextSibling) {
        if (el.nodeType==1 && selection.containsNode(el)) {
            var i = this.nodeStyle(el);
            if (index===undefined) {
                index = i;
                styletag = this.styletag;
                classname = this.classname;
            }
            if (index != i || styletag!=this.styletag || classname != this.classname) {
                mixed = true;
                break;
            }
        }
    };

    if (index===undefined) {
        index = this.nodeStyle(selNode);
    }
    this.enableOptions(this.intable);

    if (index < 0 || mixed) {
        if (mixed) {
            var caption = 'Mixed styles';
        } else if (this.styletag) {
            var caption = 'Other: ' + this.styletag + ' '+ this.classname;
        } else {
            var caption = '<no style>';
        }

        var opt = newElement('option');
        opt.text = caption;
        this.otherstyle = opt;
        this.tsselect.options.add(opt,0);
        index = 0;
    }
    this.tsselect.selectedIndex = Math.max(index,0);
};

KupuUI.prototype._cleanNode = function(node, preserveEmpty) {
            /* Clean up a block style node (e.g. P, DIV, Hn)
             * Remove trailing whitespace, then also remove up to one
             * trailing <br>
             * If the node is now empty and no preserveEmpty, remove the node itself.
             */
    function stripspace() {
        var c;
        while ((c = node.lastChild) && c.nodeType==3 && (/^\s*$/.test(c.data))) {
            node.removeChild(c);
        }
    }
    stripspace();
    var c = node.lastChild;
    if (c && c.nodeType==1 && c.tagName=='BR') {
        node.removeChild(c);
    }
    stripspace();
    if (node.childNodes.length==0 && !preserveEmpty) {
        node.parentNode.removeChild(node);
    };
};

KupuUI.prototype._cleanCell = function(eltype, classname, strip) {
    var alttype=eltype=='TD'?'TH':eltype=='TH'?'TD':null;
    
    var selNode = this.editor.getSelectedNode(true);
    var el = this.editor.getNearestParentOfType(selNode, eltype);
    if (!el && alttype) {
        // Maybe changing type
        el = this.editor.getNearestParentOfType(selNode, alttype);
    }

    //either the selection is inside a cell, spans cells, or includes
    //a collection of cells

    //first, if contained in a cell
    
    if (el) {
        if (eltype != el.tagName) {
                // Change node type.
            var node = el.ownerDocument.createElement(eltype);
            var parent = el.parentNode;
            parent.insertBefore(node, el);
            while (el.firstChild) {
                node.appendChild(el.firstChild);
            }
            parent.removeChild(el);
            el = node;
        }
            // now set the classname
        this._setClass(el, classname);
        if (strip && el.childNodes.length==1) {
            var node = el.firstChild;
            if (this.blocktagre.test(node.nodeName)) {
                for (var n = node.firstChild; n;) {
                    var nxt = n.nextSibling;
                    el.insertBefore(n, node); // Move nodes out of block
                    n = nxt;
                };
                nxt = node.nextSibling;
                el.removeChild(node);
                node = nxt;
            };
        };
    } else {
        //otherwise, find all cells that intersect the selection
        var selection = this.editor.getSelection();
        var nodes = selNode.getElementsByTagName(eltype);

        var cellNodes = [];
        for (var i = 0; i < nodes.length; i++) {
            cellNodes.push(nodes.item(i));
        };
        if (alttype) {
            nodes = selNode.getElementsByTagName(alttype);
            for (var i = 0; i < nodes.length; i++) {
                cellNodes.push(nodes.item(i));
            };
        };
        
        for (var i = 0; i < cellNodes.length; i++) {
            el = cellNodes[i];

            if(selection.intersectsNode(el)) {
                if (eltype != el.tagName) {
                    // Change node type.
                    var node = el.ownerDocument.createElement(eltype);
                    var parent = el.parentNode;
                    parent.insertBefore(node, el);
                    while (el.firstChild) {
                        node.appendChild(el.firstChild);
                    };
                    parent.removeChild(el);
                    el = node;
                };
                this._setClass(el, classname);
            }
        }
    }
};

KupuUI.prototype._setClass = function(el, classname) {
    var parent = el.parentNode;
    if (parent.tagName=='DIV') {
        // fixup buggy formatting
        var gp = parent.parentNode;
        if (el != parent.firstChild) {
            var previous = parent.cloneNode(false);
            while (el != parent.firstChild) {
                previous.appendChild(parent.firstChild);
            }
            gp.insertBefore(previous, parent);
            this._cleanNode(previous);
        }
        gp.insertBefore(el, parent);
        this._cleanNode(parent);
    };
    // now set the classname
    if (classname) {
        el.className = classname;
    } else {
        el.removeAttribute("class");
        el.removeAttribute("className");
    }
};

KupuUI.prototype._removeStyle = function() {
    var self = this;
    function needbreak(e) {
        if (isblock && e) {
            if (self.blocktagre.test(e.nodeName) || (/^br$/i.test(e.nodeName))) return;
            parent.insertBefore(ed.newElement('br'), n);
        }
    }
    var n = this.editor.getSelectedNode(true);
    var ed = this.editor;
    while(n) {
        var tag = n.nodeName.toLowerCase();
        var isblock = this.blocktagre.test(tag);
        if (this.tblre.test(tag) && n.className) {
            n.removeAttribute("class");
            n.removeAttribute("className");
            return;
        }
        if (isblock || tag == 'span') {
            var parent = n.parentNode;
            var el;
            needbreak(n.previousSibling);
            while ((el = n.firstChild)) {
                parent.insertBefore(el, n);
            }
            needbreak(n.nextSibling);
            parent.removeChild(n);
            return;
        }
        n = n.parentNode;
    };
};

KupuUI.prototype.setTextStyle = function(style, noupdate) {
    /* parse the argument into a type and classname part
       generate a block element accordingly 
    */
    var classname = '';
    var eltype = style.toUpperCase();
    if (style.indexOf('|') > -1) {
        style = style.split('|');
        eltype = style[0].toUpperCase();
        classname = style[1];
    };

    var doc = this.editor.getDocument();
    var command = eltype;
        // first create the element, then find it and set the classname
    if (this.editor.getBrowserName() == 'IE') {
        command = '<' + eltype + '>';
    };
    if (!style) {
        this._removeStyle();
    } else if (this.tblre.test(eltype)) {
        this._cleanCell(eltype, classname);
    } else if (eltype=='SPAN') {
        doc.execCommand('removeformat', null);
        if (this.editor.getBrowserName()=='IE') {
            // removeformat is broken in IE: it doesn't remove span
            // tags
            var selNode = this.editor.getSelectedNode();
            var selection = this.editor.getSelection();
            var elements = selNode.getElementsByTagName('span');
            for (var i = 0; i < elements.length; i++) {
                var span = elements[i];
                if (selection.containsNode(span)) {
                    var parent = span.parentNode;
                    while (span.firstChild) {
                        parent.insertBefore(span.firstChild, span);
                    };
                    parent.removeChild(span);
                };
            };
        }
        if (classname) {
            doc.execCommand('fontsize', '2');
            // Now convert font tags to spans
            var inner = doc.getDocument();
            var elements = inner.getElementsByTagName('FONT');
            while (elements.length > 0) {
                var font = elements[0];
                var span = inner.createElement('SPAN');
                span.className = classname;
                var parent = font.parentNode;
                parent.replaceChild(span, font);
                while (font.firstChild) {
                    span.appendChild(font.firstChild);
                };
            };
        };
    }
    else {
        doc.execCommand('formatblock', command);

            // now get a reference to the element just added
        var selNode = this.editor.getSelectedNode(true);
        var el = this.editor.getNearestParentOfType(selNode, eltype);
        if (el) {
            this._setClass(el, classname);
        } else {
            var selection = this.editor.getSelection();
            var elements = selNode.getElementsByTagName(eltype);
            for (var i = 0; i < elements.length; i++) {
                el = elements[i];
                if (selection.containsNode(el)) {
                    this._setClass(el, classname);
                }
            }
        }
    }
    if (el) {
        this.editor.getSelection().selectNodeContents(el);
    }
    if (!noupdate) {
        this.editor.updateState();
    }
};

KupuUI.prototype.createContextMenuElements = function(selNode, event) {
    var ret = [];
    ret.push(new ContextMenuElement(_('Cut'), 
                this.cutButtonHandler, this));
    ret.push(new ContextMenuElement(_('Copy'), 
                this.copyButtonHandler, this));
    ret.push(new ContextMenuElement(_('Paste'), 
                this.pasteButtonHandler, this));
    return ret;
};

KupuUI.prototype.disable = function() {
    if (this.tsselect) this.tsselect.disabled = "disabled";
};

KupuUI.prototype.enable = function() {
    if (this.tsselect) this.tsselect.disabled = "";
};

function ColorchooserTool(fgcolorbuttonid, hlcolorbuttonid, colorchooserid) {
    /* the colorchooser */
    
    this.fgcolorbutton = getFromSelector(fgcolorbuttonid);
    this.hlcolorbutton = getFromSelector(hlcolorbuttonid);
    this.ccwindow = getFromSelector(colorchooserid);
    this.command = null;
}

ColorchooserTool.prototype = new KupuTool;

ColorchooserTool.prototype.initialize = function(editor) {
    /* attach the event handlers */
    this.editor = editor;
    if (!(this.fgcolorbutton && this.hlcolorbutton && this.ccwindow)) return;
    this.createColorchooser(this.ccwindow);

    addEventHandler(this.fgcolorbutton, "click", this.openFgColorChooser, this);
    addEventHandler(this.hlcolorbutton, "click", this.openHlColorChooser, this);
    addEventHandler(this.ccwindow, "click", this.chooseColor, this);
    this.hide();
};

ColorchooserTool.prototype.updateState = function(selNode) {
    /* update state of the colorchooser */
    this.hide();
};

ColorchooserTool.prototype.openFgColorChooser = function() {
    /* event handler for opening the colorchooser */
    this.command = "forecolor";
    this.show();
};

ColorchooserTool.prototype.openHlColorChooser = function() {
    /* event handler for closing the colorchooser */
    if (this.editor.getBrowserName() == "IE") {
        this.command = "backcolor";
    } else {
        this.command = "hilitecolor";
    }
    this.show();
};

ColorchooserTool.prototype.chooseColor = function(event) {
    /* event handler for choosing the color */
    var target = _SARISSA_IS_MOZ ? event.target : event.srcElement;
    var cell = this.editor.getNearestParentOfType(target, 'td');
    var ed = this.editor;
    var doc = ed.getDocument();
    ed.execCommand('styleWithCSS', true);
    doc.execCommand(this.command, cell.bgColor);
    ed.execCommand('styleWithCSS', false);
    // this.editor.execCommand(this.command, cell.bgColor);
    this.hide();

    this.editor.logMessage(_('Color chosen'));
};

ColorchooserTool.prototype.show = function(command) {
    /* show the colorchooser */
    this.ccwindow.style.display = "block";
};

ColorchooserTool.prototype.hide = function() {
    /* hide the colorchooser */
    this.ccwindow.style.display = "none";
};

ColorchooserTool.prototype.createColorchooser = function(table) {
    /* create the colorchooser table */
    
    var chunks = ['00', '33', '66', '99', 'CC', 'FF'];
    table.setAttribute('id', 'kupu-colorchooser-table');
    table.style.borderWidth = '2px';
    table.style.borderStyle = 'solid';
    table.style.position = 'absolute';
    table.style.cursor = 'default';
    table.style.display = 'none';

    var tbody = document.createElement('tbody');

    for (var i=0; i < 6; i++) {
        var tr = document.createElement('tr');
        var r = chunks[i];
        for (var j=0; j < 6; j++) {
            var g = chunks[j];
            for (var k=0; k < 6; k++) {
                var b = chunks[k];
                var color = '#' + r + g + b;
                var td = document.createElement('td');
                td.setAttribute('bgColor', color);
                td.style.backgroundColor = color;
                td.style.borderWidth = '1px';
                td.style.borderStyle = 'solid';
                td.style.fontSize = '1px';
                td.style.width = '10px';
                td.style.height = '10px';
                var text = document.createTextNode('\u00a0');
                td.appendChild(text);
                tr.appendChild(td);
            }
        }
        tbody.appendChild(tr);
    }
    table.appendChild(tbody);

    return table;
};

ColorchooserTool.prototype.enable = function() {
    kupuButtonEnable(this.fgcolorbutton);
    kupuButtonEnable(this.hlcolorbutton);
};

ColorchooserTool.prototype.disable = function() {
    kupuButtonDisable(this.fgcolorbutton);
    kupuButtonDisable(this.hlcolorbutton);
};

function PropertyTool(titlefieldid, descfieldid) {
    /* The property tool */

    this.titlefield = getFromSelector(titlefieldid);
    this.descfield = getFromSelector(descfieldid);
};

PropertyTool.prototype = new KupuTool;

PropertyTool.prototype.initialize = function(editor) {
    /* attach the event handlers and set the initial values */
    this.editor = editor;
    addEventHandler(this.titlefield, "change", this.updateProperties, this);
    addEventHandler(this.descfield, "change", this.updateProperties, this);
    
    // set the fields
    var heads = this.editor.getInnerDocument().getElementsByTagName('head');
    if (!heads[0]) {
        this.editor.logMessage(_('No head in document!'), 1);
    } else {
        var head = heads[0];
        var titles = head.getElementsByTagName('title');
        if (titles.length) {
            this.titlefield.value = titles[0].text;
        }
        var metas = head.getElementsByTagName('meta');
        if (metas.length) {
            for (var i=0; i < metas.length; i++) {
                var meta = metas[i];
                if (meta.getAttribute('name') && 
                        meta.getAttribute('name').toLowerCase() == 
                        'description') {
                    this.descfield.value = meta.getAttribute('content');
                    break;
                }
            }
        }
    }
};

PropertyTool.prototype.updateProperties = function() {
    /* event handler for updating the properties form */
    var doc = this.editor.getInnerDocument();
    var heads = doc.getElementsByTagName('HEAD');
    if (!heads) {
        this.editor.logMessage(_('No head in document!'), 1);
        return;
    }

    var head = heads[0];

    // set the title
    var titles = head.getElementsByTagName('title');
    if (!titles) {
        var title = doc.createElement('title');
        var text = doc.createTextNode(this.titlefield.value);
        title.appendChild(text);
        head.appendChild(title);
    } else {
        var title = titles[0];
        // IE6 title has no children, and refuses appendChild.
        // Delete and recreate the title.
        if (title.childNodes.length == 0) {
            title.removeNode(true);
            title = doc.createElement('title');
            title.innerText = this.titlefield.value;
            head.appendChild(title);
        } else {
            title.childNodes[0].nodeValue = this.titlefield.value;
        }
    }
    document.title = this.titlefield.value;

    // let's just fulfill the usecase, not think about more properties
    // set the description
    var metas = doc.getElementsByTagName('meta');
    var descset = 0;
    for (var i=0; i < metas.length; i++) {
        var meta = metas[i];
        if (meta.getAttribute('name') && 
                meta.getAttribute('name').toLowerCase() == 'description') {
            meta.setAttribute('content', this.descfield.value);
            descset = 1;
        }
    }

    if (!descset) {
        var meta = doc.createElement('meta');
        meta.setAttribute('name', 'description');
        meta.setAttribute('content', this.descfield.value);
        head.appendChild(meta);
    }

    this.editor.logMessage(_('Properties modified'));
};

function LinkTool(popupurl, popupwidth, popupheight, popupprops) {
    /* Add and update hyperlinks */
    this.popupurl = popupurl || 'kupupopups/link.html';
    this.popupwidth = popupwidth || 300;
    this.popupheight = popupheight || 200;
    this.popupprops = popupprops || '';
}

LinkTool.prototype = new KupuTool;
    
LinkTool.prototype.initialize = function(editor) {
    this.editor = editor;
};

LinkTool.prototype.createLinkHandler = function(event) {
    /* create a link according to a url entered in a popup */
    var linkWindow = openPopup(this.popupurl, this.popupwidth,
                               this.popupheight, this.popupprops);
    linkWindow.linktool = this;
    linkWindow.focus();
};

LinkTool.prototype.updateLink = function (linkel, url, type, name, target, title, className, bForce) {
    if (type && type == 'anchor') {
        linkel.removeAttribute('href');
        linkel.setAttribute('name', name);
    } else {
        linkel.href = url;
        if (linkel.innerHTML == "" || (bForce && linkel.innerHTML==url)) {
            var doc = this.editor.getInnerDocument();
            while (linkel.firstChild) { linkel.removeChild(linkel.firstChild); };
            linkel.appendChild(doc.createTextNode(title || url));
        }
        if (title) {
            linkel.title = title;
        } else {
            linkel.removeAttribute('title');
        }
        if (target) {
            linkel.setAttribute('target', target);
        }
        else {
            linkel.removeAttribute('target');
        };
        if (className===undefined) {
            linkel.removeAttribute('className');
        } else {
            linkel.className = className;
        }
        linkel.style.color = this.linkcolor;
    };
};

LinkTool.prototype.formatSelectedLink = function(url, type, name, target, title, className, bForce) {
    var currnode = this.editor.getSelectedNode();

    // selection inside link
    var linkel = this.editor.getNearestParentOfType(currnode, 'A');
    if (linkel) {
        this.updateLink(linkel, url, type, name, target, title, className, bForce);
        return true;
    }

    if (currnode.nodeType!=1) return false;

    // selection contains links
    var linkelements = currnode.getElementsByTagName('A');
    var selection = this.editor.getSelection();
    var containsLink = false;
    for (var i = 0; i < linkelements.length; i++) {
        linkel = linkelements[i];
        if (selection.containsNode(linkel)) {
            this.updateLink(linkel, url, type, name, target, title, className, bForce);
            containsLink = true;
        }
    };
    return containsLink;
};

// Can create a link in the following circumstances:
//   The selection is inside a link:
//      just update the link attributes.
//   The selection contains links:
//      update the attributes of the contained links
//   No links inside or outside the selection:
//      create a link around the selection
//   No selection:
//      insert a link containing the title
//
// the order of the arguments is a bit odd here because of backward
// compatibility
LinkTool.prototype.createLink = function(url, type, name, target, title, className) {
    url = url.strip();
    if (!url) {
        this.deleteLink();
        return;
    };
    if (!this.formatSelectedLink(url, type, name, target, title, className)) {
        // No links inside or outside.
        this.editor.execCommand("CreateLink", url);
        if (!this.formatSelectedLink(url, type, name, target, title, className, true)) {
            // Insert link with no text selected, insert the title
            // or URI instead.
            var doc = this.editor.getInnerDocument();
            var linkel = doc.createElement("a");
            linkel.setAttribute('href', url);
            linkel.setAttribute('class', className || 'generated');
            this.editor.getSelection().replaceWithNode(linkel, true);
            this.updateLink(linkel, url, type, name, target, title, className);
        };
    }
};

LinkTool.prototype.deleteLink = function() {
    /* delete the current link */
    var currnode = this.editor.getSelectedNode();
    var linkel = this.editor.getNearestParentOfType(currnode, 'a');
    if (!linkel) {
        this.editor.logMessage(_('Not inside link'));
        return;
    };
    while (linkel.childNodes.length) {
        linkel.parentNode.insertBefore(linkel.childNodes[0], linkel);
    };
    linkel.parentNode.removeChild(linkel);
};

LinkTool.prototype.createContextMenuElements = function(selNode, event) {
    /* create the 'Create link' or 'Remove link' menu elements */
    var ret = [];
    var link = this.editor.getNearestParentOfType(selNode, 'a');
    if (link) {
        ret.push(new ContextMenuElement(_('Delete link'), this.deleteLink, this));
    } else {
        ret.push(new ContextMenuElement(_('Create link'), this.createLinkHandler, this));
    };
    return ret;
};

function LinkToolBox(inputid, buttonid, toolboxid, plainclass, activeclass) {
    /* create and edit links */
    
    this.input = getFromSelector(inputid);
    this.button = getFromSelector(buttonid);
    this.toolboxel = getFromSelector(toolboxid);
    this.plainclass = plainclass;
    this.activeclass = activeclass;
};

LinkToolBox.prototype = new LinkToolBox;
    
LinkToolBox.prototype.initialize = function(tool, editor) {
    /* attach the event handlers */
    this.tool = tool;
    this.editor = editor;
    if (!this.button) return;
    addEventHandler(this.input, "blur", this.updateLink, this);
    addEventHandler(this.button, "click", this.addLink, this);
};

LinkToolBox.prototype.updateState = function(selNode) {
    /* if we're inside a link, update the input, else empty it */
    var linkel = this.editor.getNearestParentOfType(selNode, 'a');
    if (linkel) {
        // check first before setting a class for backward compatibility
        if (this.toolboxel) {
            this.toolboxel.className = this.activeclass;
        };
        this.input.value = linkel.getAttribute('href');
    } else {
        // check first before setting a class for backward compatibility
        if (this.toolboxel) {
            this.toolboxel.className = this.plainclass;
        };
        this.input.value = '';
    }
};

LinkToolBox.prototype.addLink = function(event) {
    /* add a link */
    var url = this.input.value;
    this.editor.focusDocument();
    this.tool.createLink(url);
    this.editor.updateState();
};

LinkToolBox.prototype.updateLink = function() {
    /* update the current link */
    var currnode = this.editor.getSelectedNode();
    var linkel = this.editor.getNearestParentOfType(currnode, 'A');
    if (!linkel) {
        return;
    }

    var url = this.input.value;
    linkel.setAttribute('href', url);

    this.editor.updateState();
};

function ImageTool(popupurl, popupwidth, popupheight, popupprops) {
    /* Image tool to add images */
    this.popupurl = popupurl || 'kupupopups/image.html';
    this.popupwidth = popupwidth || 300;
    this.popupheight = popupheight || 200;
    this.popupprops = popupprops || '';
};

ImageTool.prototype = new KupuTool;

ImageTool.prototype.initialize = function(editor) {
    /* attach the event handlers */
    this.editor = editor;
};

ImageTool.prototype.createImageHandler = function(event) {
    /* create an image according to a url entered in a popup */
    var imageWindow = openPopup(this.popupurl, this.popupwidth,
                                this.popupheight, this.popupprops);
    imageWindow.imagetool = this;
    imageWindow.focus();
};

ImageTool.prototype.newNode = function(name, obj) {
    var ed = this.editor;
    var currobj = ed.getNearestParentOfType(ed.getSelectedNode(), name);
    if (currobj) {
        var p = currobj.parentNode;
        p.insertBefore(obj, currobj);
        p.removeChild(currobj);
        return obj;
    } else {
        return ed.insertNodeAtSelection(obj, 1);
    }
};

ImageTool.prototype.createImage = function(url, alttext, imgclass) {
    /* create an image */
    var img = this.editor.getInnerDocument().createElement('img');
    img.src = url;
    img.setAttribute('kupu-src', url);
    img.removeAttribute('height');
    img.removeAttribute('width');
    if (alttext) {
        img.alt = alttext;
    };
    if (imgclass) {
        img.className = imgclass;
    };
    this.newNode('IMG', img);
    return img;
};

ImageTool.prototype.create_flash = function(url, alttext, className, width, height) {
    var ed = this.editor;
    var obj = ed.newElement('object',
        {src:url, alt:alttext, className:className, width:width,
         height:height, type:'application/x-shockwave-flash',
         'data':url},
        [ed.newElement('param', {name:'movie', value:url})]);
    this.newNode('OBJECT', obj);
};

ImageTool.prototype.setImageClass = function(imgclass) {
    /* set the class of the selected image */
    var currnode = this.editor.getSelectedNode();
    var currimg = this.editor.getNearestParentOfType(currnode, 'IMG');
    if (currimg) {
        currimg.className = imgclass;
    };
};

ImageTool.prototype.createContextMenuElements = function(selNode, event) {
    return [new ContextMenuElement(_('Create image'), this.createImageHandler,
                                   this)];
};

function ImageToolBox(inputfieldid, insertbuttonid, classselectid, toolboxid,
                      plainclass, activeclass) {
    /* toolbox for adding images */

    this.inputfield = getFromSelector(inputfieldid);
    this.insertbutton = getFromSelector(insertbuttonid);
    this.classselect = getFromSelector(classselectid);
    this.toolboxel = getFromSelector(toolboxid);
    this.plainclass = plainclass;
    this.activeclass = activeclass;
};

ImageToolBox.prototype = new KupuToolBox;

ImageToolBox.prototype.initialize = function(tool, editor) {
    this.tool = tool;
    this.editor = editor;
    addEventHandler(this.classselect, "change", this.setImageClass, this);
    addEventHandler(this.insertbutton, "click", this.addImage, this);
};

ImageToolBox.prototype.updateState = function(selNode, event) {
    /* update the state of the toolbox element */
    var imageel = this.editor.getNearestParentOfType(selNode, 'img');
    if (imageel) {
        // check first before setting a class for backward compatibility
        if (this.toolboxel) {
            this.toolboxel.className = this.activeclass;
            this.inputfield.value = imageel.getAttribute('src');
            var imgclass = imageel.className ? imageel.className : 'image-inline';
            selectSelectItem(this.classselect, imgclass);
        };
    } else {
        if (this.toolboxel) {
            this.toolboxel.className = this.plainclass;
        };
    };
};

ImageToolBox.prototype.addImage = function() {
    /* add an image */
    var url = this.inputfield.value;
    var sel_class = this.classselect.options[this.classselect.selectedIndex].value;
    this.editor.focusDocument();
    this.tool.createImage(url, null, sel_class);
    this.editor.updateState();
};

ImageToolBox.prototype.setImageClass = function() {
    /* set the class for the current image */
    var sel_class = this.classselect.options[this.classselect.selectedIndex].value;
    this.editor.focusDocument();
    this.tool.setImageClass(sel_class);
    this.editor.updateState();
};

function TableTool() {
    /* The table tool */
};

TableTool.prototype = new KupuTool;

// XXX There are some awfully long methods in here!!
TableTool.prototype.createContextMenuElements = function(selNode, event) {
    var table =  this.editor.getNearestParentOfType(selNode, 'table');
    if (!table) {
        var ret = [];
        var el = new ContextMenuElement(_('Add table'), this.addPlainTable, this);
        ret.push(el);
        return ret;
    } else {
        var ret = [];
        ret.push(new ContextMenuElement(_('Add row'), this.addTableRow, this));
        ret.push(new ContextMenuElement(_('Delete row'), this.delTableRow, this));
        ret.push(new ContextMenuElement(_('Add column'), this.addTableColumn, this));
        ret.push(new ContextMenuElement(_('Delete column'), this.delTableColumn, this));
        ret.push(new ContextMenuElement(_('Delete Table'), this.delTable, this));
        return ret;
    };
};

TableTool.prototype.addPlainTable = function() {
    /* event handler for the context menu */
    this.createTable(2, 3, 1, 'plain');
};

TableTool.prototype.createTable = function(rows, cols, makeHeader, tableclass) {
    /* add a table */
    if (rows < 1 || rows > 99 || cols < 1 || cols > 99) {
        this.editor.logMessage(_('Invalid table size'), 1);
        return;
    };

    var doc = this.editor.getInnerDocument();
    var table = doc.createElement("table");
    table.className = tableclass;

    // If the user wants a row of headings, make them
    if (makeHeader) {
        var tr = doc.createElement("tr");
        var thead = doc.createElement("thead");
        for (var i=1; i <= cols; i++) {
            var th = doc.createElement("th");
            th.appendChild(doc.createTextNode("Col " + i));
            tr.appendChild(th);
        }
        thead.appendChild(tr);
        table.appendChild(thead);
    }

    var tbody = doc.createElement("tbody");
    for (var i=0; i < rows; i++) {
        var tr = doc.createElement("tr");
        for (var j=0; j < cols; j++) {
            var td = doc.createElement("td");
            var content = doc.createTextNode('\u00a0');
            td.appendChild(content);
            tr.appendChild(td);
        }
        tbody.appendChild(tr);
    }
    table.appendChild(tbody);
    this.editor.insertNodeAtSelection(table);

    this._setTableCellHandlers(table);
    return table;
};

TableTool.prototype._setTableCellHandlers = function(table) {
    // make each cell select its full contents if it's clicked
    addEventHandler(table, 'click', this._selectContentIfEmpty, this);

    var cells = table.getElementsByTagName('td');
    for (var i=0; i < cells.length; i++) {
        addEventHandler(cells[i], 'click', this._selectContentIfEmpty, this);
    };
    
    // select the nbsp in the first cell
    var firstcell = cells[0];
    if (firstcell) {
        var children = firstcell.childNodes;
        if (children.length == 1 && children[0].nodeType == 3 && 
                children[0].nodeValue == '\xa0') {
            var selection = this.editor.getSelection();
            selection.selectNodeContents(firstcell);
        };
    };
};

TableTool.prototype._selectContentIfEmpty = function() {
    var selNode = this.editor.getSelectedNode();
    var cell = this.editor.getNearestParentOfType(selNode, 'td');
    if (!cell) {
        return;
    };
    var children = cell.childNodes;
    if (children.length == 1 && children[0].nodeType == 3 && 
            children[0].nodeValue == '\xa0') {
        var selection = this.editor.getSelection();
        selection.selectNodeContents(cell);
    };
};

TableTool.prototype.addTableRow = function() {
    /* Find the current row and add a row after it */
    var currnode = this.editor.getSelectedNode();
    var currtbody = this.editor.getNearestParentOfType(currnode, "TBODY");
    var bodytype = "tbody";
    if (!currtbody) {
        currtbody = this.editor.getNearestParentOfType(currnode, "THEAD");
        bodytype = "thead";
    }
    var parentrow = this.editor.getNearestParentOfType(currnode, "TR");
    var nextrow = parentrow.nextSibling;

    // get the number of cells we should place
    var colcount = 0;
    for (var i=0; i < currtbody.childNodes.length; i++) {
        var el = currtbody.childNodes[i];
        if (el.nodeType != 1) {
            continue;
        }
        if (el.nodeName.toLowerCase() == 'tr') {
            var cols = 0;
            for (var j=0; j < el.childNodes.length; j++) {
                if (el.childNodes[j].nodeType == 1) {
                    cols++;
                }
            }
            if (cols > colcount) {
                colcount = cols;
            }
        }
    }

    var newrow = this.editor.getInnerDocument().createElement("TR");

    for (var i = 0; i < colcount; i++) {
        var newcell;
        if (bodytype == 'tbody') {
            newcell = this.editor.getInnerDocument().createElement("TD");
        } else {
            newcell = this.editor.getInnerDocument().createElement("TH");
        }
        var newcellvalue = this.editor.getInnerDocument().createTextNode("\u00a0");
        newcell.appendChild(newcellvalue);
        newrow.appendChild(newcell);
    }

    if (!nextrow) {
        currtbody.appendChild(newrow);
    } else {
        currtbody.insertBefore(newrow, nextrow);
    }
};

TableTool.prototype.delTableRow = function() {
    /* Find the current row and delete it */
    var currnode = this.editor.getSelectedNode();
    var parentrow = this.editor.getNearestParentOfType(currnode, "TR");
    if (!parentrow) {
        this.editor.logMessage(_('No row to delete'), 1);
        return;
    }

    // move selection aside
    // XXX: doesn't work if parentrow is the only row of thead/tbody/tfoot
    // XXX: doesn't preserve the colindex
    var selection = this.editor.getSelection();
    if (parentrow.nextSibling) {
        selection.selectNodeContents(parentrow.nextSibling.firstChild);
    } else if (parentrow.previousSibling) {
        selection.selectNodeContents(parentrow.previousSibling.firstChild);
    };

    // remove the row
    parentrow.parentNode.removeChild(parentrow);
};

TableTool.prototype.addTableColumn = function() {
    /* Add a new column after the current column */
    var currnode = this.editor.getSelectedNode();
    var currtd = this.editor.getNearestParentOfType(currnode, 'TD');
    if (!currtd) {
        currtd = this.editor.getNearestParentOfType(currnode, 'TH');
    }
    if (!currtd) {
        this.editor.logMessage(_('No parentcolumn found!'), 1);
        return;
    }
    var currtable = this.editor.getNearestParentOfType(currnode, 'TABLE');
    
    // get the current index
    var tdindex = this._getColIndex(currtd);

    // now add a column to all rows
    // first the thead
    var theads = currtable.getElementsByTagName('THEAD');
    if (theads) {
        for (var i=0; i < theads.length; i++) {
            // let's assume table heads only have ths
            var currthead = theads[i];
            for (var j=0; j < currthead.childNodes.length; j++) {
                var tr = currthead.childNodes[j];
                if (tr.nodeType != 1) {
                    continue;
                }
                var currindex = 0;
                for (var k=0; k < tr.childNodes.length; k++) {
                    var th = tr.childNodes[k];
                    if (th.nodeType != 1) {
                        continue;
                    }
                    if (currindex == tdindex) {
                        var doc = this.editor.getInnerDocument();
                        var newth = doc.createElement('th');
                        var text = doc.createTextNode('\u00a0');
                        newth.appendChild(text);
                        if (tr.childNodes.length == k+1) {
                            // the column will be on the end of the row
                            tr.appendChild(newth);
                        } else {
                            tr.insertBefore(newth, tr.childNodes[k + 1]);
                        }
                        break;
                    }
                    currindex++;
                }
            }
        }
    }

    // then the tbody
    var tbodies = currtable.getElementsByTagName('TBODY');
    if (tbodies) {
        for (var i=0; i < tbodies.length; i++) {
            // let's assume table heads only have ths
            var currtbody = tbodies[i];
            for (var j=0; j < currtbody.childNodes.length; j++) {
                var tr = currtbody.childNodes[j];
                if (tr.nodeType != 1) {
                    continue;
                }
                var currindex = 0;
                for (var k=0; k < tr.childNodes.length; k++) {
                    var td = tr.childNodes[k];
                    if (td.nodeType != 1) {
                        continue;
                    }
                    if (currindex == tdindex) {
                        var doc = this.editor.getInnerDocument();
                        var newtd = doc.createElement('td');
                        var text = doc.createTextNode('\u00a0');
                        newtd.appendChild(text);
                        if (tr.childNodes.length == k+1) {
                            // the column will be on the end of the row
                            tr.appendChild(newtd);
                        } else {
                            tr.insertBefore(newtd, tr.childNodes[k + 1]);
                        }
                        break;
                    }
                    currindex++;
                }
            }
        }
    }
};

TableTool.prototype.delTableColumn = function() {
    /* remove a column */
    var currnode = this.editor.getSelectedNode();
    var currtd = this.editor.getNearestParentOfType(currnode, 'TD');
    if (!currtd) {
        currtd = this.editor.getNearestParentOfType(currnode, 'TH');
    }
    var currcolindex = this._getColIndex(currtd);
    var currtable = this.editor.getNearestParentOfType(currnode, 'TABLE');

    // move selection aside
    var selection = this.editor.getSelection();
    if (currtd.nextSibling) {
        selection.selectNodeContents(currtd.nextSibling);
    } else if (currtd.previousSibling) {
        selection.selectNodeContents(currtd.previousSibling);
    };

    // remove the theaders
    var heads = currtable.getElementsByTagName('THEAD');
    if (heads.length) {
        for (var i=0; i < heads.length; i++) {
            var thead = heads[i];
            for (var j=0; j < thead.childNodes.length; j++) {
                var tr = thead.childNodes[j];
                if (tr.nodeType != 1) {
                    continue;
                }
                var currindex = 0;
                for (var k=0; k < tr.childNodes.length; k++) {
                    var th = tr.childNodes[k];
                    if (th.nodeType != 1) {
                        continue;
                    }
                    if (currindex == currcolindex) {
                        tr.removeChild(th);
                        break;
                    }
                    currindex++;
                }
            }
        }
    }

    // now we remove the column field, a bit harder since we need to take 
    // colspan and rowspan into account XXX Not right, fix theads as well
    var bodies = currtable.getElementsByTagName('TBODY');
    for (var i=0; i < bodies.length; i++) {
        var currtbody = bodies[i];
        for (var j=0; j < currtbody.childNodes.length; j++) {
            var tr = currtbody.childNodes[j];
            if (tr.nodeType != 1) {
                continue;
            }
            var currindex = 0;
            for (var k=0; k < tr.childNodes.length; k++) {
                var cell = tr.childNodes[k];
                if (cell.nodeType != 1) {
                    continue;
                }
                if (currindex == currcolindex) {
                    tr.removeChild(cell);
                    break;
                }
                currindex++;
            }
        }
    }
};

TableTool.prototype.delTable = function() {
    /* delete the current table */
    var currnode = this.editor.getSelectedNode();
    var table = this.editor.getNearestParentOfType(currnode, 'table');
    if (!table) {
        this.editor.logMessage(_('Not inside a table!'));
        return;
    };
    table.parentNode.removeChild(table);
};

TableTool.prototype.setColumnAlign = function(newalign) {
    /* change the alignment of a full column */
    var currnode = this.editor.getSelectedNode();
    var currtd = this.editor.getNearestParentOfType(currnode, "TD");
    var bodytype = 'tbody';
    if (!currtd) {
        currtd = this.editor.getNearestParentOfType(currnode, "TH");
        bodytype = 'thead';
    }
    var currcolindex = this._getColIndex(currtd);
    var currtable = this.editor.getNearestParentOfType(currnode, "TABLE");

    // unfortunately this is not enough to make the browsers display
    // the align, we need to set it on individual cells as well and
    // mind the rowspan...
    for (var i=0; i < currtable.childNodes.length; i++) {
        var currtbody = currtable.childNodes[i];
        if (currtbody.nodeType != 1 || 
                (/^thead|tbody$/i.test(currtbody.nodeName))) {
            continue;
        }
        for (var j=0; j < currtbody.childNodes.length; j++) {
            var row = currtbody.childNodes[j];
            if (row.nodeType != 1) {
                continue;
            }
            var index = 0;
            for (var k=0; k < row.childNodes.length; k++) {
                var cell = row.childNodes[k];
                if (cell.nodeType != 1) {
                    continue;
                }
                if (index == currcolindex) {
                    if (this.editor.config.use_css) {
                        cell.style.textAlign = newalign;
                    } else {
                        cell.setAttribute('align', newalign);
                    }
                    cell.className = 'align-' + newalign;
                }
                index++;
            }
        }
    }
};

TableTool.prototype.setTableClass = function(sel_class) {
    /* set the class for the table */
    var currnode = this.editor.getSelectedNode();
    var currtable = this.editor.getNearestParentOfType(currnode, 'TABLE');

    if (currtable) {
        currtable.className = sel_class;
    }
};

TableTool.prototype._getColIndex = function(currcell) {
    /* Given a node, return an integer for which column it is */
    var prevsib = currcell.previousSibling;
    var currcolindex = 0;
    while (prevsib) {
        if (prevsib.nodeType == 1 && 
                (prevsib.tagName.toUpperCase() == "TD" || 
                    prevsib.tagName.toUpperCase() == "TH")) {
            var colspan = prevsib.colSpan;
            if (colspan) {
                currcolindex += parseInt(colspan);
            } else {
                currcolindex++;
            }
        }
        prevsib = prevsib.previousSibling;
        if (currcolindex > 30) {
            alert("Recursion detected when counting column position");
            return;
        }
    }

    return currcolindex;
};

TableTool.prototype._getColumnAlign = function(selNode) {
    /* return the alignment setting of the current column */
    var align;
    var td = this.editor.getNearestParentOfType(selNode, 'td');
    if (!td) {
        td = this.editor.getNearestParentOfType(selNode, 'th');
    };
    if (td) {
        align = td.getAttribute('align');
        if (this.editor.config.use_css) {
            align = td.style.textAlign;
        };
    };
    return align;
};

TableTool.prototype.fixTable = function(event) {
    /* fix the table so it can be processed by Kupu */
    // since this can be quite a nasty creature we can't just use the
    // helper methods
    
    // first we create a new tbody element
    var currnode = this.editor.getSelectedNode();
    var table = this.editor.getNearestParentOfType(currnode, 'TABLE');
    if (!table) {
        this.editor.logMessage(_('Not inside a table!'));
        return;
    };
    this._fixTableHelper(table);
};

TableTool.prototype._isBodyRow = function(row) {
    for (var node = row.firstChild; node; node=node.nextSibling) {
        if (/^td$/i.test(node.nodeName)) {
            return true;
        }
    }
    return false;
};

TableTool.prototype._cleanCell = function(el) {
    // Remove formatted div or p from a cell
    var nxt, n;
    for (var node = el.firstChild; node;) {
        if (/^div|p$/i.test(node.nodeName)) {
            for (var n = node.firstChild; n;) {
                var nxt = n.nextSibling;
                el.insertBefore(n, node); // Move nodes out of div
                n = nxt;
            }
            nxt = node.nextSibling;
            el.removeChild(node);
            node = nxt;
        } else {
            node = node.nextSibling;
        }
    }
    var c;
    while (el.firstChild && (c = el.firstChild).nodeType==3 && (/^\s+/.test(c.data))) {
        c.data = c.data.replace(/^\s+/, '');
        if (!c.data) {
            el.removeChild(c);
        } else {
            break;
        };
    };
    while (el.lastChild && (c = el.lastChild).nodeType==3 && (/\s+$/.test(c.data))) {
        c.data = c.data.replace(/\s+$/, '');
        if (!c.data) {
            el.removeChild(c);
        } else {
            break;
        };
    };
    el.removeAttribute('colSpan');
    el.removeAttribute('rowSpan');
};

TableTool.prototype._countCols = function(rows, numcols) {
    for (var i=0; i < rows.length; i++) {
        var row = rows[i];
        var currnumcols = 0;
        for (var node = row.firstChild; node; node=node.nextSibling) {
            if (/^(td|th)$/i.test(node.nodeName)) {
                currnumcols += parseInt(node.getAttribute('colSpan') || '1');
            };
        };
        if (currnumcols > numcols) {
            numcols = currnumcols;
        };
    };
    return numcols;
};

TableTool.prototype._cleanRows = function(rows, container, numcols) {
    // now walk through all rows to clean them up
    for (var i=0; i < rows.length; i++) {
        var row = rows[i];
        var doc = this.editor.getInnerDocument();
        var newrow = doc.createElement('tr');
        if (row.className) {
            newrow.className = row.className;
        }
        for (var node = row.firstChild; node;) {
            var nxt = node.nextSibling;
            if (/^(td|th)$/i.test(node.nodeName)) {
                this._cleanCell(node);
                newrow.appendChild(node);
            };
            node = nxt;
        };
        if (newrow.childNodes.length) {
            container.appendChild(newrow);
        };
    };
    // now make sure all rows have the correct length
    for (var row = container.firstChild; row; row=row.nextSibling) {
        var cellname = row.lastChild.nodeName;
        while (row.childNodes.length < numcols) {
            var cell = doc.createElement(cellname);
            var nbsp = doc.createTextNode('\u00a0');
            cell.appendChild(nbsp);
            row.appendChild(cell);
        };
    };
};

TableTool.prototype._fixTableHelper = function(table) {
    /* the code to actually fix tables */
    var doc = this.editor.getInnerDocument();
    var thead = doc.createElement('thead');
    var tbody = doc.createElement('tbody');
    var tfoot = doc.createElement('tfoot');

    var table_classes = this.editor.config.table_classes;
    function cleanClassName(name) {
        var allowed_classes = table_classes['class'];
        for (var i = 0; i < allowed_classes.length; i++) {
            var classname = allowed_classes[i];
            classname = classname.classname || classname;
            if (classname==name) return name;
        };
        return allowed_classes[0];
    }
    if (table_classes) {
        table.className = cleanClassName(table.className);
    } else {
        table.removeAttribute('class');
        table.removeAttribute('className');
    };
    table.removeAttribute('border');
    table.removeAttribute('cellpadding');
    table.removeAttribute('cellPadding');
    table.removeAttribute('cellspacing');
    table.removeAttribute('cellSpacing');

    // now get all the rows of the table, the rows can either be
    // direct descendants of the table or inside a 'tbody', 'thead'
    // or 'tfoot' element

    var hrows = [], brows = [], frows = [];
    for (var node = table.firstChild; node; node = node.nextSibling) {
        var nodeName = node.nodeName.toLowerCase();
        if (/tr/i.test(node.nodeName)) {
            brows.push(node);
        } else if (/thead|tbody|tfoot/i.test(node.nodeName)) {
            var rows = nodeName=='thead' ? hrows : nodeName=='tfoot' ? frows : brows;
            for (var inode = node.firstChild; inode; inode = inode.nextSibling) {
                if (/tr/i.test(inode.nodeName)) {
                    rows.push(inode);
                };
            };
        };
    };
    /* Extract thead and tfoot from tbody */
    while (brows.length && !this._isBodyRow(brows[0])) {
        hrows.push(brows[0]);
        brows.shift();
    }
    while (brows.length && !this._isBodyRow(brows[brows.length-1])) {
        var last = brows[brows.length-1];
        brows.length -= 1;
        frows.unshift(last);
    }
    // now find out how many cells our rows should have
    var numcols = this._countCols(hrows, 0);
    numcols = this._countCols(brows, numcols);
    numcols = this._countCols(frows, numcols);

    // now walk through all rows to clean them up
    this._cleanRows(hrows, thead);
    this._cleanRows(brows, tbody);
    this._cleanRows(frows, tfoot);

    // now remove all the old stuff from the table and add the new
    // tbody
    while (table.firstChild) {
        table.removeChild(table.firstChild);
    }
    if (hrows.length) {
        table.appendChild(thead);
    }
    if (brows.length) {
        table.appendChild(tbody);
    }
    if (frows.length) {
        table.appendChild(tfoot);
    }
};

TableTool.prototype.fixAllTables = function() {
    /* fix all the tables in the document at once */
    var tables = this.editor.getInnerDocument().getElementsByTagName('table');
    for (var i=0; i < tables.length; i++) {
        this._fixTableHelper(tables[i]);
    };
};

function TableToolBox(addtabledivid, edittabledivid, newrowsinputid, 
                    newcolsinputid, makeheaderinputid, classselectid, alignselectid, addtablebuttonid,
                    addrowbuttonid, delrowbuttonid, addcolbuttonid, delcolbuttonid, fixbuttonid,
                    delbuttonid, fixallbuttonid, toolboxid, plainclass, activeclass) {
    /* The table tool */

    // a lot of dependencies on html elements here, but most implementations
    // will use them all I guess
    this.addtablediv = getFromSelector(addtabledivid);
    this.edittablediv = getFromSelector(edittabledivid);
    this.newrowsinput = getFromSelector(newrowsinputid);
    this.newcolsinput = getFromSelector(newcolsinputid);
    this.makeheaderinput = getFromSelector(makeheaderinputid);
    this.classselect = getFromSelector(classselectid);
    this.alignselect = getFromSelector(alignselectid);
    this.addtablebutton = getFromSelector(addtablebuttonid);
    this.addrowbutton = getFromSelector(addrowbuttonid);
    this.delrowbutton = getFromSelector(delrowbuttonid);
    this.addcolbutton = getFromSelector(addcolbuttonid);
    this.delcolbutton = getFromSelector(delcolbuttonid);
    this.fixbutton = getFromSelector(fixbuttonid);
    this.delbutton = getFromSelector(delbuttonid);
    this.fixallbutton = getFromSelector(fixallbuttonid);
    this.toolboxel = getFromSelector(toolboxid);
    this.plainclass = plainclass;
    this.activeclass = activeclass;
};

TableToolBox.prototype = new KupuToolBox;

// register event handlers
TableToolBox.prototype.initialize = function(tool, editor) {
    /* attach the event handlers */
    this.tool = tool;
    this.editor = editor;
    // build the select list of table classes if configured
    if (this.editor.config.table_classes) {
        var classes = this.editor.config.table_classes['class'];
        while (this.classselect.hasChildNodes()) {
            this.classselect.removeChild(this.classselect.firstChild);
        };
        for (var i=0; i < classes.length; i++) {
            var classname = classes[i];
            classname = classname.classname || classname;
            var option = document.createElement('option');
            var content = document.createTextNode(classname);
            option.appendChild(content);
            option.setAttribute('value', classname);
            this.classselect.appendChild(option);
        };
    };
    addEventHandler(this.addtablebutton, "click", this.addTable, this);
    addEventHandler(this.addrowbutton, "click", this.addTableRow, this);
    addEventHandler(this.delrowbutton, "click", this.delTableRow, this);
    addEventHandler(this.addcolbutton, "click", this.addTableColumn, this);
    addEventHandler(this.delcolbutton, "click", this.delTableColumn, this);
    addEventHandler(this.alignselect, "change", this.setColumnAlign, this);
    addEventHandler(this.classselect, "change", this.setTableClass, this);
    addEventHandler(this.fixbutton, "click", this.fixTable, this);
    addEventHandler(this.delbutton, "click", this.delTable, this);
    addEventHandler(this.fixallbutton, "click", this.fixAllTables, this);
    this.addtablediv.style.display = "block";
    this.edittablediv.style.display = "none";
};

TableToolBox.prototype.updateState = function(selNode) {
    /* update the state (add/edit) and update the pulldowns (if required) */
    var table = this.editor.getNearestParentOfType(selNode, 'table');
    if (table) {
        this.addtablediv.style.display = "none";
        this.edittablediv.style.display = "block";

        var align = this.tool._getColumnAlign(selNode);
        selectSelectItem(this.alignselect, align);
        selectSelectItem(this.classselect, table.className);
        if (this.toolboxel) {
            this.toolboxel.className = this.activeclass;
        };
    } else {
        this.edittablediv.style.display = "none";
        this.addtablediv.style.display = "block";
        this.alignselect.selectedIndex = 0;
        this.classselect.selectedIndex = 0;
        if (this.toolboxel) {
            this.toolboxel.className = this.plainclass;
        };
    };
};

TableToolBox.prototype.addTable = function() {
    /* add a table */
    var rows = this.newrowsinput.value;
    var cols = this.newcolsinput.value;
    var makeHeader = this.makeheaderinput.checked;
    var tableclass = this.classselect.options[this.classselect.selectedIndex].value;

    this.tool.createTable(rows, cols, makeHeader, tableclass);
    this.editor.focusDocument();
    this.editor.updateState();
};

TableToolBox.prototype.setColumnAlign = function() {
    /* set the alignment of the current column */
    var newalign = this.alignselect.options[this.alignselect.selectedIndex].value;
    this.editor.focusDocument();
    this.tool.setColumnAlign(newalign);
    this.editor.updateState();
};

TableToolBox.prototype.setTableClass = function() {
    /* set the class for the current table */
    var sel_class = this.classselect.options[this.classselect.selectedIndex].value;
    if (sel_class) {
        this.editor.focusDocument();
        this.tool.setTableClass(sel_class);
        this.editor.updateState();
    };
};

TableToolBox.prototype.addTableRow = function() {
    this.editor.focusDocument();
    this.tool.addTableRow();
    this.editor.updateState();
};

TableToolBox.prototype.delTableRow = function() {
    this.editor.focusDocument();
    this.tool.delTableRow();
    this.editor.updateState();
};

TableToolBox.prototype.addTableColumn = function() {
    this.editor.focusDocument();
    this.tool.addTableColumn();
    this.editor.updateState();
};

TableToolBox.prototype.delTableColumn = function() {
    this.editor.focusDocument();
    this.tool.delTableColumn();
    this.editor.updateState();
};

TableToolBox.prototype.fixTable = function() {
    this.editor.focusDocument();
    this.tool.fixTable();
    this.editor.updateState();
};

TableToolBox.prototype.fixAllTables = function() {
    this.editor.focusDocument();
    this.tool.fixAllTables();
    this.editor.updateState();
};

TableToolBox.prototype.delTable = function() {
    this.editor.focusDocument();
    this.tool.delTable();
    this.editor.updateState();
};

function ListTool(addulbuttonid, addolbuttonid, ulstyleselectid, olstyleselectid) {
    /* tool to set list styles */

    this.addulbutton = getFromSelector(addulbuttonid);
    this.addolbutton = getFromSelector(addolbuttonid);
    this.ulselect = getFromSelector(ulstyleselectid);
    this.olselect = getFromSelector(olstyleselectid);
    
    this.style_to_type = {'decimal': '1',
                            'lower-alpha': 'a',
                            'upper-alpha': 'A',
                            'lower-roman': 'i',
                            'upper-roman': 'I',
                            'disc': 'disc',
                            'square': 'square',
                            'circle': 'circle',
                            'none': 'none'
                            };
    this.type_to_style = {'1': 'decimal',
                            'a': 'lower-alpha',
                            'A': 'upper-alpha',
                            'i': 'lower-roman',
                            'I': 'upper-roman',
                            'disc': 'disc',
                            'square': 'square',
                            'circle': 'circle',
                            'none': 'none'
                            };
};

ListTool.prototype = new KupuTool;

ListTool.prototype.initialize = function(editor) {
    /* attach event handlers */
    this.editor = editor;
    if (this.addulbutton) {
        addEventHandler(this.addulbutton, "click", this.addUnorderedList, this);
    }
    if (this.addolbutton) {
        addEventHandler(this.addolbutton, "click", this.addOrderedList, this);
    }
    if (this.ulselect) {
        addEventHandler(this.ulselect, "change", this.setUnorderedListStyle, this);
        this.ulselect.style.display = "none";
    }
    if (this.olselect) {
        addEventHandler(this.olselect, "change", this.setOrderedListStyle, this);
        this.olselect.style.display = "none";
    }
};

ListTool.prototype._handleStyles = function(currnode, onselect, offselect) {
    if (this.editor.config.use_css) {
        var currstyle = currnode.style.listStyleType;
    } else {
        var currstyle = this.type_to_style[currnode.getAttribute('type')];
    }
    if (onselect) {
        selectSelectItem(onselect, currstyle);
        onselect.style.display = "inline";
    }
    if (offselect) {
        offselect.style.display = "none";
        offselect.selectedIndex = 0;
    }
};

ListTool.prototype.updateState = function(selNode) {
    /* update the visibility and selection of the list type pulldowns */
    // we're going to walk through the tree manually since we want to 
    // check on 2 items at the same time
    for (var currnode=selNode; currnode; currnode=currnode.parentNode) {
        var tag = currnode.nodeName.toLowerCase();
        if (tag == 'ul') {
            this._handleStyles(currnode, this.ulselect, this.olselect);
            return;
        } else if (tag == 'ol') {
            this._handleStyles(currnode, this.olselect, this.ulselect);
            return;
        }
    }
    if (this.ulselect) {
        this.ulselect.selectedIndex = 0;
        this.ulselect.style.display = "none";
    };
    if (this.olselect) {
        this.olselect.selectedIndex = 0;
        this.olselect.style.display = "none";
    };
};

ListTool.prototype.addList = function(command) {
    if (this.ulselect) this.ulselect.style.display = "inline";
    if (this.olselect) this.olselect.style.display = "none";
    this.editor.execCommand(command);
    this.editor.focusDocument();
};

ListTool.prototype.addUnorderedList = function() {
    /* add an unordered list */
    this.addList("insertunorderedlist");
};

ListTool.prototype.addOrderedList = function() {
    /* add an ordered list */
    this.addList("insertorderedlist");
};

ListTool.prototype.setListStyle = function(tag, select) {
    /* set the type of an ul */
    if (!select) return;
    var currnode = this.editor.getSelectedNode();
    var l = this.editor.getNearestParentOfType(currnode, tag);
    var style = select.options[select.selectedIndex].value;
    if (this.editor.config.use_css) {
        l.style.listStyleType = style;
    } else {
        l.setAttribute('type', this.style_to_type[style]);
    }
    this.editor.focusDocument();
};

ListTool.prototype.setUnorderedListStyle = function() {
    /* set the type of an ul */
    this.setListStyle('ul', this.ulselect);
};

ListTool.prototype.setOrderedListStyle = function() {
    /* set the type of an ol */
    this.setListStyle('ol', this.olselect);
};

ListTool.prototype.enable = function() {
    kupuButtonEnable(this.addulbutton);
    kupuButtonEnable(this.addolbutton);
    if (this.ulselect) this.ulselect.disabled = "";
    if (this.olselect) this.olselect.disabled = "";
};

ListTool.prototype.disable = function() {
    kupuButtonDisable(this.addulbutton);
    kupuButtonDisable(this.addolbutton);
    if (this.ulselect) this.ulselect.disabled = "disabled";
    if (this.olselect) this.olselect.disabled = "disabled";
};

function ShowPathTool() {
    /* shows the path to the current element in the status bar */
};

ShowPathTool.prototype = new KupuTool;

ShowPathTool.prototype.updateState = function(selNode) {
    /* calculate and display the path */
    var path = '';
    var url = null; // for links we want to display the url too
    var currnode = selNode;
    var nn;
    while (currnode != null && (nn=currnode.nodeName.toLowerCase()) != '#document') {
        if (nn=='a') {
            url = currnode.getAttribute('href');
        };
        path = '/' + nn + path;
        currnode = currnode.parentNode;
    }
    
    try {
        window.status = url ? 
                (path.toString() + ' - contains link to \'' + 
                    url.toString() + '\'') :
                path;
    } catch (e) {
        this.editor.logMessage(_('Could not set status bar message, ' +
                                'check your browser\'s security settings.'), 1);
    };
};

function ViewSourceTool() {
    /* tool to provide a 'show source' context menu option */
    this.sourceWindow = null;
};

ViewSourceTool.prototype = new KupuTool;
    
ViewSourceTool.prototype.viewSource = function() {
    /* open a window and write the current contents of the iframe to it */
    if (this.sourceWindow) {
        this.sourceWindow.close();
    };
    this.sourceWindow = window.open('#', 'sourceWindow');
    
    //var transform = this.editor._filterContent(this.editor.getInnerDocument().documentElement);
    //var contents = transform.xml; 
    var contents = '<html>\n' + this.editor.getInnerDocument().documentElement.innerHTML + '\n</html>';
    
    var doc = this.sourceWindow.document;
    doc.write('\xa0');
    doc.close();
    var body = doc.getElementsByTagName("body")[0];
    while (body.hasChildNodes()) {
        body.removeChild(body.firstChild);
    };
    var pre = doc.createElement('pre');
    var textNode = doc.createTextNode(contents);
    body.appendChild(pre);
    pre.appendChild(textNode);
};

ViewSourceTool.prototype.createContextMenuElements = function(selNode, event) {
    /* create the context menu element */
    return [new ContextMenuElement(_('View source'), this.viewSource, this)];
};

function DefinitionListTool(dlbuttonid) {
    /* a tool for managing definition lists

        the dl elements should behave much like plain lists, and the keypress
        behaviour should be similar
    */

    this.dlbutton = getFromSelector(dlbuttonid);
};

DefinitionListTool.prototype = new KupuTool;

DefinitionListTool.prototype.initialize = function(editor) {
    /* initialize the tool */
    this.editor = editor;
    if (!this.dlbutton) return;
    addEventHandler(this.dlbutton, 'click', this.createDefinitionList, this);
    addEventHandler(editor.getInnerDocument(), 'keyup', this._keyDownHandler, this);
    addEventHandler(editor.getInnerDocument(), 'keypress', this._keyPressHandler, this);
};

// even though the following methods may seem view related, they belong 
// here, since they describe core functionality rather then view-specific
// stuff
DefinitionListTool.prototype.handleEnterPress = function(selNode) {
    var dl = this.editor.getNearestParentOfType(selNode, 'dl');
    if (dl) {
        var dt = this.editor.getNearestParentOfType(selNode, 'dt');
        if (dt) {
            if (dt.childNodes.length == 1 && dt.childNodes[0].nodeValue == '\xa0') {
                this.escapeFromDefinitionList(dl, dt, selNode);
                return;
            };

            var selection = this.editor.getSelection();
            var startoffset = selection.startOffset();
            var endoffset = selection.endOffset(); 
            if (endoffset > startoffset) {
                // throw away any selected stuff
                selection.cutChunk(startoffset, endoffset);
                selection = this.editor.getSelection();
                startoffset = selection.startOffset();
            };
            
            var ellength = selection.getElementLength(selection.parentElement());
            if (startoffset >= ellength - 1) {
                // create a new element
                this.createDefinition(dl, dt);
            } else {
                var doc = this.editor.getInnerDocument();
                var newdt = selection.splitNodeAtSelection(dt);
                var newdd = doc.createElement('dd');
                while (newdt.hasChildNodes()) {
                    if (newdt.firstChild != newdt.lastChild || newdt.firstChild.nodeName.toLowerCase() != 'br') {
                        newdd.appendChild(newdt.firstChild);
                    };
                };
                newdt.parentNode.replaceChild(newdd, newdt);
                selection.selectNodeContents(newdd);
                selection.collapse();
            };
        } else {
            var dd = this.editor.getNearestParentOfType(selNode, 'dd');
            if (!dd) {
                this.editor.logMessage(_('Not inside a definition list element!'));
                return;
            };
            if (dd.childNodes.length == 1 && dd.childNodes[0].nodeValue == '\xa0') {
                this.escapeFromDefinitionList(dl, dd, selNode);
                return;
            };
            var selection = this.editor.getSelection();
            var startoffset = selection.startOffset();
            var endoffset = selection.endOffset();
            if (endoffset > startoffset) {
                // throw away any selected stuff
                selection.cutChunk(startoffset, endoffset);
                selection = this.editor.getSelection();
                startoffset = selection.startOffset();
            };
            var ellength = selection.getElementLength(selection.parentElement());
            if (startoffset >= ellength - 1) {
                // create a new element
                this.createDefinitionTerm(dl, dd);
            } else {
                // add a break and continue in this element
                var br = this.editor.getInnerDocument().createElement('br');
                this.editor.insertNodeAtSelection(br, 1);
                //var selection = this.editor.getSelection();
                //selection.moveStart(1);
                selection.collapse(true);
            };
        };
    };
};

DefinitionListTool.prototype.handleTabPress = function(selNode) {
};

DefinitionListTool.prototype._keyDownHandler = function(event) {
    var selNode = this.editor.getSelectedNode();
    var dl = this.editor.getNearestParentOfType(selNode, 'dl');
    if (!dl) {
        return;
    };
    if (event.keyCode) {
        if (event.preventDefault) {
            event.preventDefault();
        } else {
            event.returnValue = false;
        };
    };
};

DefinitionListTool.prototype._keyPressHandler = function(event) {
    var selNode = this.editor.getSelectedNode();
    var dl = this.editor.getNearestParentOfType(selNode, 'dl');
    if (!dl) {
        return;
    };
    switch (event.keyCode) {
        case 13:
            this.handleEnterPress(selNode);
            if (event.preventDefault) {
                event.preventDefault();
            } else {
                event.returnValue = false;
            };
            break;
        case 9:
            if (event.preventDefault) {
                event.preventDefault();
            } else {
                event.returnValue = false;
            };
            this.handleTabPress(selNode);
    };
};

DefinitionListTool.prototype.createDefinitionList = function() {
    /* create a new definition list (dl) */
    var selection = this.editor.getSelection();
    var doc = this.editor.getInnerDocument();

    var selection = this.editor.getSelection();
    var cloned = selection.cloneContents();
    // first get the 'first line' (until the first break) and use it
    // as the dt's content
    var iterator = new NodeIterator(cloned);
    var currnode = null;
    var remove = false;
    while ((currnode = iterator.next())) {
        if (currnode.nodeName.toLowerCase() == 'br') {
            remove = true;
        };
        if (remove) {
            var next = currnode;
            while (!next.nextSibling) {
                next = next.parentNode;
            };
            next = next.nextSibling;
            iterator.setCurrent(next);
            currnode.parentNode.removeChild(currnode);
        };
    };

    var dtcontentcontainer = cloned;
    var collapsetoend = false;
    
    var dl = doc.createElement('dl');
    this.editor.insertNodeAtSelection(dl);
    var dt = this.createDefinitionTerm(dl);
    if (dtcontentcontainer.hasChildNodes()) {
        collapsetoend = true;
        while (dt.hasChildNodes()) {
            dt.removeChild(dt.firstChild);
        };
        while (dtcontentcontainer.hasChildNodes()) {
            dt.appendChild(dtcontentcontainer.firstChild);
        };
    };

    var selection = this.editor.getSelection();
    selection.selectNodeContents(dt);
    selection.collapse(collapsetoend);
};

DefinitionListTool.prototype.createDefinitionTerm = function(dl, dd) {
    /* create a new definition term inside the current dl */
    var doc = this.editor.getInnerDocument();
    var dt = doc.createElement('dt');
    // somehow Mozilla seems to add breaks to all elements...
    if (dd) {
        if (dd.lastChild.nodeName.toLowerCase() == 'br') {
            dd.removeChild(dd.lastChild);
        };
    };
    // dd may be null here, if so we assume this is the first element in 
    // the dl
    if (!dd || dl == dd.lastChild) {
        dl.appendChild(dt);
    } else {
        var nextsibling = dd.nextSibling;
        if (nextsibling) {
            dl.insertBefore(dt, nextsibling);
        } else {
            dl.appendChild(dt);
        };
    };
    var nbsp = doc.createTextNode('\xa0');
    dt.appendChild(nbsp);
    var selection = this.editor.getSelection();
    selection.selectNodeContents(dt);
    selection.collapse();

    this.editor.focusDocument();
    return dt;
};

DefinitionListTool.prototype.createDefinition = function(dl, dt, initial_content) {
    var doc = this.editor.getInnerDocument();
    var dd = doc.createElement('dd');
    var nextsibling = dt.nextSibling;
    // somehow Mozilla seems to add breaks to all elements...
    if (dt) {
        if (dt.lastChild.nodeName.toLowerCase() == 'br') {
            dt.removeChild(dt.lastChild);
        };
    };
    while (nextsibling) {
        var name = nextsibling.nodeName.toLowerCase();
        if (name == 'dd' || name == 'dt') {
            break;
        } else {
            nextsibling = nextsibling.nextSibling;
        };
    };
    if (nextsibling) {
        dl.insertBefore(dd, nextsibling);
        //this._fixStructure(doc, dl, nextsibling);
    } else {
        dl.appendChild(dd);
    };
    if (initial_content) {
        for (var i=0; i < initial_content.length; i++) {
            dd.appendChild(initial_content[i]);
        };
    };
    var nbsp = doc.createTextNode('\xa0');
    dd.appendChild(nbsp);
    var selection = this.editor.getSelection();
    selection.selectNodeContents(dd);
    selection.collapse();
};

DefinitionListTool.prototype.escapeFromDefinitionList = function(dl, currel, selNode) {
    var doc = this.editor.getInnerDocument();
    var p = doc.createElement('p');
    var nbsp = doc.createTextNode('\xa0');
    p.appendChild(nbsp);

    if (dl.lastChild == currel) {
        dl.parentNode.insertBefore(p, dl.nextSibling);
    } else {
        for (var i=0; i < dl.childNodes.length; i++) {
            var child = dl.childNodes[i];
            if (child == currel) {
                var newdl = this.editor.getInnerDocument().createElement('dl');
                while (currel.nextSibling) {
                    newdl.appendChild(currel.nextSibling);
                };
                dl.parentNode.insertBefore(newdl, dl.nextSibling);
                dl.parentNode.insertBefore(p, dl.nextSibling);
            };
        };
    };
    currel.parentNode.removeChild(currel);
    var selection = this.editor.getSelection();
    selection.selectNodeContents(p);
    selection.collapse();
    this.editor.focusDocument();
};

DefinitionListTool.prototype._fixStructure = function(doc, dl, offsetnode) {
    /* makes sure the order of the elements is correct */
    var currname = offsetnode.nodeName.toLowerCase();
    var currnode = offsetnode.nextSibling;
    while (currnode) {
        if (currnode.nodeType == 1) {
            var nodename = currnode.nodeName.toLowerCase();
            if (currname == 'dt' && nodename == 'dt') {
                var dd = doc.createElement('dd');
                while (currnode.hasChildNodes()) {
                    dd.appendChild(currnode.childNodes[0]);
                };
                currnode.parentNode.replaceChild(dd, currnode);
            } else if (currname == 'dd' && nodename == 'dd') {
                var dt = doc.createElement('dt');
                while (currnode.hasChildNodes()) {
                    dt.appendChild(currnode.childNodes[0]);
                };
                currnode.parentNode.replaceChild(dt, currnode);
            };
        };
        currnode = currnode.nextSibling;
    };
};

function KupuZoomTool(buttonid, firsttab, lasttab) {
    this.button = getFromSelector(buttonid);
    firsttab = firsttab || 'kupu-tb-styles';
    lasttab = lasttab || 'kupu-logo-button';

    this.initialize = function(editor) {
        this.offclass = 'kupu-zoom';
        this.onclass = 'kupu-zoom-pressed';
        this.pressed = false;
        if (!this.button) return;
        this.baseinitialize(editor);
        addEventHandler(window, "resize", this.onresize, this);
        addEventHandler(window, "scroll", this.onscroll, this);

        /* Toolbar tabbing */
        var lastbutton = getFromSelector(lasttab);
        var firstbutton = getFromSelector(firsttab);
        var iframe = editor.getInnerDocument();
        this.setTabbing(iframe, firstbutton, lastbutton);
        this.setTabbing(firstbutton, null, editor.getDocument().getWindow());
    };
};

KupuZoomTool.prototype = new KupuLateFocusStateButton;
KupuZoomTool.prototype.baseinitialize = KupuZoomTool.prototype.initialize;

KupuZoomTool.prototype.onscroll = function() {
    if (!this.zoomed) return;
    /* XXX Problem here: Mozilla doesn't generate onscroll when window is
     * scrolled by focus move or selection. */
    var top = window.pageYOffset!=undefined ? window.pageYOffset : document.documentElement.scrollTop;
    var left = window.pageXOffset!=undefined ? window.pageXOffset : document.documentElement.scrollLeft;
    if (top || left) window.scrollTo(0, 0);
};

// Handle tab pressed from a control.
KupuZoomTool.prototype.setTabbing = function(control, forward, backward) {
    function TabDown(event) {
        if (event.keyCode != 9 || !this.zoomed) return;

        var target = event.shiftKey ? backward : forward;
        if (!target) return;

        if (event.stopPropogation) event.stopPropogation();
        event.cancelBubble = true;
        event.returnValue = false;

        target.focus();
        return false;
    }
    addEventHandler(control, "keydown", TabDown, this);
};

KupuZoomTool.prototype.onresize = function() {
    if (!this.zoomed) return;

    var editor = this.editor;
    var iframe = editor.getDocument().editable;
    var sourcetool = editor.getTool('sourceedittool');
    var sourceArea = sourcetool?sourcetool.getSourceArea():null;
    var fulleditor = iframe.parentNode;

    if (window.innerWidth) {
        var width = window.innerWidth;
        var height = window.innerHeight;
    } else if (document.documentElement) {
        if (!window._IE_VERSION) {
            _IE_VERSION = /MSIE\s*([0-9.]*)/.exec(navigator.appVersion);
        };
        var kludge = (_IE_VERSION[1]<7)?5:0;
        var width = document.documentElement.offsetWidth-kludge;
        var height = document.documentElement.offsetHeight-kludge;
    } else {
        var width = document.body.offsetWidth-5;
        var height = document.body.offsetHeight-5;
    }
    var offset = fulleditor.offsetTop;
    var nheight = Math.max(height - offset -1/*top border*/, 10) + 'px';
    width = width + 'px';
    fulleditor.style.width = width; /*IE needs this*/
    iframe.style.width = width;
    iframe.style.height = nheight;
    if (sourceArea) {
        sourceArea.style.width = width;
        sourceArea.style.height = nheight;
    }
};

KupuZoomTool.prototype.checkfunc = function(selNode, button, editor, event) {
    return this.zoomed;
};

KupuZoomTool.prototype.commandfunc = function(button, editor) {
    /* Toggle zoom state */
    var zoom = button.pressed;
    this.zoomed = zoom;

    var zoomClass = 'kupu-fulleditor-zoomed';
    var iframe = editor.getDocument().getEditable();

    var body = document.body;
    var html = document.getElementsByTagName('html')[0];
    var doc = editor.getInnerDocument();
    if (zoom) {
        html.style.overflow = 'hidden';
        window.scrollTo(0, 0);
        editor.setClass(zoomClass);
        body.className += ' '+zoomClass;
        doc.body.className += ' '+zoomClass;
        this.onresize();
    } else {
        html.style.overflow = '';
        var fulleditor = iframe.parentNode;
        fulleditor.style.width = '';
        body.className = body.className.replace(/ *kupu-fulleditor-zoomed/, '');
        doc.body.className = doc.body.className.replace(/ *kupu-fulleditor-zoomed/, '');
        editor.clearClass(zoomClass);

        iframe.style.width = '';
        iframe.style.height = '';

        var sourcetool = editor.getTool('sourceedittool');
        var sourceArea = sourcetool?sourcetool.getSourceArea():null;
        if (sourceArea) {
            sourceArea.style.width = '';
            sourceArea.style.height = '';
        };
    }
    // Mozilla needs this. Yes, really!
    doc.designMode=doc.designMode;

    window.scrollTo(0, iframe.offsetTop);
    editor.focusDocument();
};

/* The anchor tool */
function AnchorTool() {};
AnchorTool.prototype = new LinkTool;
var proto = AnchorTool.prototype;

proto.fillStyleSelect = function(select) {
    var ui = this.editor.getTool('ui');
    var options = ui.getStyles()[0];

    for (var i = 1; i < options.length-1; i++) {
        var cur = options[i];
        var opt = document.createElement('option');
        opt.text = cur[0];
        opt.value = cur[1];
        select.options.add(opt);
    }
};

proto.grubParas = function(style1, style2) {
    var doc = this.editor.getInnerDocument();
    var body = doc.body;
    var paras = [];
    var pat = /(.*?)(\|.*|$)/;
    var tag1 = style1.replace(pat, '$1');
    var tag2 = style2.replace(pat, '$1');
    if (tag2 && tag2 != tag1) { tag1 = "*"; }
    if (tag1) {
        var nodes = body.getElementsByTagName(tag1);
        for (var i = 0; i < nodes.length; i++) {
            var node = nodes[i];
            var style = node.nodeName.toLowerCase() + "|" + node.className;
            if (style==style1) {
                paras.push([node,0]);
            }
            if (style==style2) {
                paras.push([node,1]);
            }
        }
    }
    return paras;
};

proto.getAnchorsInUse = function() {
    var doc = this.editor.getInnerDocument();
    var anchors = doc.getElementsByTagName('a');
    var inuse = {};
    for (var i = 0; i < anchors.length; i++) {
        var m = (/(.*)(#.*)$/.exec(anchors[i].href));
        /* TODO: filter out external links */
        if (m) { inuse[decodeURIComponent(m[2])] = 1; };
    }
    return inuse;
};

proto.removeAnchor = function(node) {
    var anchors = node.getElementsByTagName('a');
    if (anchors.length > 0) {
        var anchor = anchors[0];
        anchor.removeAttribute('name');
        if (anchor.href) return;
        anchor.parentNode.removeChild(anchor);
    };
};

proto.getAnchor = function(node, ifexists) {
    /* Returns the anchor for a node, creating one if reqd. unless
     * ifexists is set*/
    var anchors = node.getElementsByTagName('a');
    for (var i = 0; i < anchors.length; i++) {
        if (anchors[i].name) { return anchors[i].name; }
    }

    if (ifexists) return;

    var anchor = Sarissa.getText(node, true).strip().truncate(40).
        replace(/[^\w]+/g, '-').toLowerCase().replace(/-$/,'') || 'anchor';
    anchor = anchor.replace(/^((?:[^-]*-){0,3}[^-]*)(.*)$/,'$1');

    var unique = 0;
    var existing = this.editor.getInnerDocument().anchors;
    for (var i = 0; i < existing.length; i++) {
        var name = existing[i].name;
        if (name==anchor) {
            if (unique==0) unique = -1;
        } else if (name.length > anchor.length && name.substring(0,anchor.length)==anchor) {
            var tail = name.substring(anchor.length);
            tail = parseInt(tail);
            if (tail <= unique) {
                unique = tail-1;
            }
        }
    }
    if (unique) anchor += unique.toString();
    node.insertBefore(this.editor.newElement('a', {'name': anchor}),
        node.firstChild);
    return anchor;
};

/* IE doesn't have a dump function */
if (window.dump===undefined) {
    var dump = function(msg) { };
};
proto.createContextMenuElements = function() {
    return [];
};



/* - kupuloggers.js - */
/*****************************************************************************
 *
 * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/
// $Id: kupuloggers.js 35854 2006-12-18 15:37:15Z duncan $


//----------------------------------------------------------------------------
// Loggers
//
//  Loggers are pretty simple classes, that should have 1 method, called 
//  'log'. This is called with 2 arguments, the first one is the message to
//  log and the second is the severity, which can be 0 or some other false
//  value for debug messages, 1 for warnings and 2 for errors (the loggers
//  are allowed to raise an exception if that happens).
//
//----------------------------------------------------------------------------

function DebugLogger() {
    /* Alert all messages */
    
    this.log = function(message, severity) {
        /* log a message */
        if (severity > 1) {
            alert("Error: " + message);
        } else if (severity == 1) {
            alert("Warning: " + message);
        } else {
            alert("Log message: " + message);
        }
    };
}

function PlainLogger(debugelid, maxlength) {
    /* writes messages to a debug tool and throws errors */

    this.debugel = getFromSelector(debugelid);
    this.maxlength = maxlength;
    
    this.log = function(message, severity) {
        /* log a message */
        if (severity > 1) {
            throw message;
        } else {
            if (this.maxlength) {
                if (this.debugel.childNodes.length > this.maxlength - 1) {
                    this.debugel.removeChild(this.debugel.childNodes[0]);
                }
            }
            var now = new Date();
            var time = now.getHours() + ':' + now.getMinutes() + ':' + now.getSeconds();
            
            var div = document.createElement('div');
            var text = document.createTextNode(time + ' - ' + message);
            div.appendChild(text);
            this.debugel.appendChild(div);
        }
    };
}

function DummyLogger() {
    this.log = function(message, severity) {
        if (severity > 1) {
            throw message;
        }
    };
}


/* - kupucontentfilters.js - */
/*****************************************************************************
 *
 * Copyright (c) 2003-2004 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/

// $Id: kupucontentfilters.js 56537 2008-07-14 13:06:56Z duncan $


//----------------------------------------------------------------------------
// 
// ContentFilters
//
//  These are (or currently 'this is') filters for HTML cleanup and 
//  conversion. Kupu filters should be classes that should get registered to
//  the editor using the registerFilter method with 2 methods: 'initialize'
//  and 'filter'. The first will be called with the editor as its only
//  argument and the latter with a reference to the ownerdoc (always use 
//  that to create new nodes and such) and the root node of the HTML DOM as 
//  its arguments.
//
//----------------------------------------------------------------------------

function NonXHTMLTagFilter() {
    /* filter out non-XHTML tags*/
    
    // A mapping from element name to whether it should be left out of the 
    // document entirely. If you want an element to reappear in the resulting 
    // document *including* it's contents, add it to the mapping with a 1 value.
    // If you want an element not to appear but want to leave it's contents in 
    // tact, add it to the mapping with a 0 value. If you want an element and
    // it's contents to be removed from the document, don't add it.
    if (arguments.length) {
        // allow an optional filterdata argument
        this.filterdata = arguments[0];
    } else {
        // provide a default filterdata dict
        this.filterdata = {'html': 1,
                            'body': 1,
                            'head': 1,
                            'title': 1,
                            
                            'a': 1,
                            'abbr': 1,
                            'acronym': 1,
                            'address': 1,
                            'b': 1,
                            'base': 1,
                            'big': 1,
                            'blockquote': 1,
                            'br': 1,
                            'caption': 1,
                            'cite': 1,
                            'code': 1,
                            'col': 1,
                            'colgroup': 1,
                            'dd': 1,
                            'dfn': 1,
                            'div': 1,
                            'dl': 1,
                            'dt': 1,
                            'em': 1,
                            'h1': 1,
                            'h2': 1,
                            'h3': 1,
                            'h4': 1,
                            'h5': 1,
                            'h6': 1,
                            'i': 1,
                            'img': 1,
                            'kbd': 1,
                            'li': 1,
                            'link': 1,
                            'meta': 1,
                            'ol': 1,
                            'p': 1,
                            'pre': 1,
                            'q': 1,
                            'samp': 1,
                            'script': 1,
                            'small': 1,
                            'span': 1,
                            'strong': 1,
                            'style': 1,
                            'sub': 1,
                            'sup': 1,
                            'table': 1,
                            'tbody': 1,
                            'td': 1,
                            'tfoot': 1,
                            'th': 1,
                            'thead': 1,
                            'tr': 1,
                            'tt': 1,
                            'ul': 1,
                            'u': 1,
                            'var': 1,

                            // even though they're deprecated we should leave
                            // font tags as they are, since Kupu sometimes
                            // produces them itself.
                            'font': 1,
                            'center': 0
                            };
    };
                        
    this.initialize = function(editor) {
        /* init */
        this.editor = editor;
    };

    this.filter = function(ownerdoc, htmlnode) {
        return this._filterHelper(ownerdoc, htmlnode);
    };

    this._filterHelper = function(ownerdoc, node) {
        /* filter unwanted elements */
        if (node.nodeType == 3) {
            return ownerdoc.createTextNode(node.nodeValue);
        } else if (node.nodeType == 4) {
            return ownerdoc.createCDATASection(node.nodeValue);
        };
        // create a new node to place the result into
        // XXX this can be severely optimized by doing stuff inline rather 
        // than on creating new elements all the time!
        var newnode = ownerdoc.createElement(node.nodeName);
        // copy the attributes
        for (var i=0; i < node.attributes.length; i++) {
            var attr = node.attributes[i];
            newnode.setAttribute(attr.nodeName, attr.nodeValue);
        };
        for (var i=0; i < node.childNodes.length; i++) {
            var child = node.childNodes[i];
            var nodeType = child.nodeType;
            var nodeName = child.nodeName.toLowerCase();
            if (nodeType == 3 || nodeType == 4) {
                newnode.appendChild(this._filterHelper(ownerdoc, child));
            };
            if (nodeName in this.filterdata && this.filterdata[nodeName]) {
                newnode.appendChild(this._filterHelper(ownerdoc, child));
            } else if (nodeName in this.filterdata) {
                for (var j=0; j < child.childNodes.length; j++) {
                    newnode.appendChild(this._filterHelper(ownerdoc, 
                        child.childNodes[j]));
                };
            };
        };
        return newnode;
    };
};

//-----------------------------------------------------------------------------
//
// XHTML validation support
//
// This class is the XHTML 1.0 transitional DTD expressed as Javascript
// data structures.
//
function XhtmlValidation(editor) {
    // Support functions
    function asList(s) {
        if (typeof(s)==typeof('') || !s.length) s = [s];
        return s;
    }

    this.Set = function(ary) {
        if (typeof(ary)==typeof('')) ary = [ary];
        if (ary instanceof Array) {
            for (var i = 0; i < ary.length; i++) {
                this[ary[i]] = 1;
            }
        }
        else {
            for (var v in ary) { // already a set?
                this[v] = 1;
            }
        }
    };
    
    this._exclude = function(array, exceptions) {
        var ex;
        if (exceptions.split) {
            ex = exceptions.split("|");
        } else {
            ex = exceptions;
        }
        var exclude = new this.Set(ex);
        var res = [];
        for (var k=0; k < array.length;k++) {
            if (!exclude[array[k]]) res.push(array[k]);
        }
        return res;
    };
    this.setAttrFilter = function(attributes, filter) {
        for (var j = 0; j < attributes.length; j++) {
            var attr = attributes[j];
            this.attrFilters[attr] = filter || this._defaultCopyAttribute;
        }
    };

    this.setTagAttributes = function(tags, attributes) {
        for (var j = 0; j < tags.length; j++) {
            this.tagAttributes[tags[j]] = attributes;
        }
    };

    // define some new attributes for existing tags
    this.includeTagAttributes = function(tags, attributes) {
        for (var j = 0; j < tags.length; j++) {
            var tag = tags[j];
            this.tagAttributes[tag] = this.tagAttributes[tag].concat(attributes);
        }
    };

    this.excludeTagAttributes = function(tags, attributes) {
        var bad = new this.Set(attributes);
        var tagset = new this.Set(tags);
        for (var tag in tagset) {
            var val = this.tagAttributes[tag];
            if (val) {
                for (var i = val.length; i >= 0; i--) {
                    if (bad[val[i]]) {
                        val = val.concat(); // Copy
                        val.splice(i,1);
                    }
                }
            }
            this.tagAttributes[tag] = val;
            // have to store this to allow filtering for nodes on which
            // '*' is set as allowed, this allows using '*' for the attributes
            // but also filtering some out
            this.badTagAttributes[tag] = attributes;
        }
    };

    this.excludeTags = function(badtags) {
        if (typeof(badtags)==typeof('')) badtags = [badtags];
        for (var i = 0; i < badtags.length; i++) {
            delete this.tagAttributes[badtags[i]];
        }
    };

    this.excludeAttributes = function(badattrs) {
        this.excludeTagAttributes(this.tagAttributes, badattrs);
        for (var i = 0; i < badattrs.length; i++) {
            delete this.attrFilters[badattrs[i]];
        }
    };
    var replaceNodes = { 'b': 'strong', 'i': 'em' };
    if (editor.getBrowserName()=="IE") {
        this._getTagName = function(htmlnode) {
            var nodename = htmlnode.nodeName.toLowerCase();
            if (htmlnode.scopeName && htmlnode.scopeName != "HTML") {
                nodename = htmlnode.scopeName+':'+nodename;
            }
            return replaceNodes[nodename]||nodename;
        };
    } else {
        this._getTagName = function(htmlnode) {
            var nodename = htmlnode.nodeName.toLowerCase();
            return replaceNodes[nodename]||nodename;
        };
    };

    // Supporting declarations
    this.elements = new function(validation) {
        // A list of all attributes
        this.attributes = [
            'abbr','accept','accept-charset','accesskey','action','align','alink',
            'alt','archive','axis','background','bgcolor','border','cellpadding',
            'cellspacing','char','charoff','charset','checked','cite','class',
            'classid','clear','code','codebase','codetype','color','cols','colspan',
            'compact','content','coords','data','datetime','declare','defer','dir',
            'disabled','enctype','face','for','frame','frameborder','headers',
            'height','href','hreflang','hspace','http-equiv','id','ismap','label',
            'lang','language','link','longdesc','marginheight','marginwidth',
            'maxlength','media','method','multiple','name','nohref','noshade','nowrap',
            'object','onblur','onchange','onclick','ondblclick','onfocus','onkeydown',
            'onkeypress','onkeyup','onload','onmousedown','onmousemove','onmouseout',
            'onmouseover','onmouseup','onreset','onselect','onsubmit','onunload',
            'profile','prompt','readonly','rel','rev','rows','rowspan','rules',
            'scheme','scope','scrolling','selected','shape','size','span','src',
            'standby','start','style','summary','tabindex','target','text','title',
            'type','usemap','valign','value','valuetype','vlink','vspace','width',
            'xml:lang','xml:space','xmlns'];

        // Core attributes
        this.coreattrs = ['id', 'title', 'style', 'class'];
        this.i18n = ['lang', 'dir', 'xml:lang'];
        // All event attributes are here but commented out so we don't
        // have to remove them later.
        this.events = []; // 'onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup'.split('|');
        this.focusevents = []; // ['onfocus','onblur']
        this.loadevents = []; // ['onload', 'onunload']
        this.formevents = []; // ['onsubmit','onreset']
        this.inputevents = [] ; // ['onselect', 'onchange']
        this.focus = ['accesskey', 'tabindex'].concat(this.focusevents);
        this.attrs = [].concat(this.coreattrs, this.i18n, this.events);

        // entities
        this.special_extra = ['object','applet','img','map','iframe', 'embed'];
        this.special_basic=['br','span','bdo'];
        this.special = [].concat(this.special_basic, this.special_extra);
        this.fontstyle_extra = ['big','small','font','basefont'];
        this.fontstyle_basic = ['tt','i','b','u','s','strike'];
        this.fontstyle = [].concat(this.fontstyle_basic, this.fontstyle_extra);
        this.phrase_extra = ['sub','sup'];
        this.phrase_basic = ['em','strong','dfn','code','q',
                             'samp','kbd','var','cite','abbr','acronym'];
        this.phrase = [].concat(this.phrase_basic, this.phrase_extra);
        this.inline_forms = ['input','select','textarea','label','button'];
        this.misc_inline = ['ins','del', 'script'];
        this.misc = ['noscript'].concat(this.misc_inline);
        this.inline = ['a'].concat(this.special, this.fontstyle, this.phrase, this.inline_forms);

        this.Inline = ['#text', '#comment'].concat(this.inline, this.misc_inline);

        this.heading = ['h1','h2','h3','h4','h5','h6'];
        this.lists = ['ul','ol','dl','menu','dir'];
        this.blocktext = ['pre','hr','blockquote','address','center','noframes'];
        this.block = ['p','div','isindex','fieldset','table'].concat(
                     this.heading, this.lists, this.blocktext);

        this.Flow = ['#text','form'].concat(this.block, this.inline, this.misc);
    }(this);

    this._commonsetting = function(self, names, value) {
        for (var n = 0; n < names.length; n++) {
            self[names[n]] = value;
        }
    };
    
    // The tagAttributes class returns all valid attributes for a tag,
    // e.g. a = this.tagAttributes.head
    // a.head -> [ 'lang', 'xml:lang', 'dir', 'id', 'profile' ]
    this.tagAttributes = new function(el, validation) {
        this.title = el.i18n.concat('id');
        this.html = this.title.concat('xmlns');
        this.head = this.title.concat('profile');
        this.base = ['id', 'href', 'target'];
        this.meta =  this.title.concat('http-equiv','name','content', 'scheme');
        this.link = el.attrs.concat('charset','href','hreflang','type', 'rel','rev','media','target');
        this.style = this.title.concat('type','media','title', 'xml:space');
        this.script = ['id','charset','type','language','src','defer', 'xml:space'];
        this.iframe = [
                      'longdesc','name','src','frameborder','marginwidth',
                      'marginheight','scrolling','align','height','width'].concat(el.coreattrs);
        this.body = ['background','bgcolor','text','link','vlink','alink'].concat(el.attrs, el.loadevents);
        validation._commonsetting(this,
                                  ['p','div'].concat(el.heading),
                                  ['align'].concat(el.attrs));
        this.dl = this.dir = this.menu = el.attrs.concat('compact');
        this.ul = this.menu.concat('type');
        this.ol = this.ul.concat('start');
        this.li = el.attrs.concat('type','value');
        this.hr = el.attrs.concat('align','noshade','size','width');
        this.pre = el.attrs.concat('width','xml:space');
        this.blockquote = this.q = el.attrs.concat('cite');
        this.ins = this.del = this.blockquote.concat('datetime');
        this.a = el.attrs.concat(el.focus,'charset','type','name','href','hreflang','rel','rev','shape','coords','target');
        this.bdo = el.coreattrs.concat(el.events, 'lang','xml:lang','dir');
        this.br = el.coreattrs.concat('clear');
        validation._commonsetting(this,
                                  ['noscript','noframes','dt', 'dd', 'address','center','span','em', 'strong', 'dfn','code',
                                  'samp','kbd','var','cite','abbr','acronym','sub','sup','tt',
                                  'i','b','big','small','u','s','strike', 'fieldset'],
                                  el.attrs);

        this.basefont = ['id','size','color','face'];
        this.font = el.coreattrs.concat(el.i18n, 'size','color','face');
        this.object = el.attrs.concat('declare','classid','codebase','data','type','codetype','archive','standby','height','width','usemap','name','tabindex','align','border','hspace','vspace');
        this.embed=['*'];
        this.param = ['id','name','value','valuetype','type'];
        this.applet = el.coreattrs.concat('codebase','archive','code','object','alt','name','width','height','align','hspace','vspace');
        this.img = el.attrs.concat('src','alt','name','longdesc','height','width','usemap','ismap','align','border','hspace','vspace');
        this.map = this.title.concat('title','name', 'style', 'class', el.events);
        this.area = el.attrs.concat('shape','coords','href','nohref','alt','target', el.focus);
        this.form = el.attrs.concat('action','method','name','enctype',el.formevents,'accept','accept-charset','target');
        this.label = el.attrs.concat('for','accesskey', el.focusevents);
        this.input = el.attrs.concat('type','name','value','checked','disabled','readonly','size','maxlength','src','alt','usemap',el.input,'accept','align', el.focus);
        this.select = el.attrs.concat('name','size','multiple','disabled','tabindex', el.focusevents,el.input);
        this.optgroup = el.attrs.concat('disabled','label');
        this.option = el.attrs.concat('selected','disabled','label','value');
        this.textarea = el.attrs.concat('name','rows','cols','disabled','readonly', el.inputevents, el.focus);
        this.legend = el.attrs.concat('accesskey','align');
        this.button = el.attrs.concat('name','value','type','disabled',el.focus);
        this.isindex = el.coreattrs.concat('prompt', el.i18n);
        this.table = el.attrs.concat('summary','width','border','frame','rules','cellspacing','cellpadding','align','bgcolor');
        this.caption = el.attrs.concat('align');
        this.col = this.colgroup = el.attrs.concat('span','width','align','char','charoff','valign');
        this.thead =  el.attrs.concat('align','char','charoff','valign');
        this.tfoot = this.tbody = this.thead;
        this.tr = this.thead.concat('bgcolor');
        this.td = this.th = this.tr.concat('abbr','axis','headers','scope','rowspan','colspan','nowrap','width','height');
    }(this.elements, this);

    this.badTagAttributes = new this.Set({});

    // Nasty tags should be initialised from Plone's HTML control panel
    // but we have a few tags we know for sure aren't going to work
    // so we can put them in whatever.
    this.nastyTags = new this.Set({'script':1, 'style':1, 'meta':1, 'title':1}); 

    // State array. For each tag identifies what it can contain.
    // I'm not attempting to check the order or number of contained
    // tags (yet).
    this.States = new function(el, validation) {

        var here = this;
        function setStates(tags, value) {
            var valset = new validation.Set(value);

            for (var i = 0; i < tags.length; i++) {
                here[tags[i]] = valset;
            }
        }
        
        setStates(['html'], ['head','body']);
        setStates(['head'], ['title','base','script','style', 'meta','link','object','isindex']);
        setStates([
            'base', 'meta', 'link', 'hr', 'param', 'img', 'area', 'input',
            'br', 'basefont', 'isindex', 'col'], []);

        setStates(['title','style','script','option','textarea'], ['#text']);
        setStates([ 'noscript', 'iframe', 'noframes', 'body', 'div',
            'li', 'dd', 'blockquote', 'center', 'ins', 'del', 'td', 'th'], el.Flow);

        setStates(el.heading, el.Inline);
        setStates([ 'p', 'dt', 'address', 'span', 'bdo', 'caption',
            'em', 'strong', 'dfn','code','samp','kbd','var',
            'cite','abbr','acronym','q','sub','sup','tt','i',
            'b','big','small','u','s','strike','font','label',
            'legend'], el.Inline);

        setStates(['ul', 'ol', 'menu', 'dir', 'ul'], ['li']);
        setStates(['dl'], ['dt','dd']);
        setStates(['pre'], validation._exclude(el.Inline, "img|object|embed|applet|big|small|sub|sup|font|basefont"));
        setStates(['a'], validation._exclude(el.Inline, "a"));
        setStates(['applet', 'object','embed'], ['#text', 'param','form'].concat(el.block, el.inline, el.misc));
        setStates(['map'], ['form', 'area'].concat(el.block, el.misc));
        setStates(['form'], validation._exclude(el.Flow, ['form']));
        setStates(['select'], ['optgroup','option']);
        setStates(['optgroup'], ['option']);
        setStates(['fieldset'], ['#text','legend','form'].concat(el.block,el.inline,el.misc));
        setStates(['button'], validation._exclude(el.Flow, ['a','form','iframe'].concat(el.inline_forms)));
        setStates(['table'], ['caption','col','colgroup','thead','tfoot','tbody','tr']);
        setStates(['thead', 'tfoot', 'tbody'], ['tr']);
        setStates(['colgroup'], ['col']);
        setStates(['tr'], ['th','td']);
    }(this.elements, this);

    // Permitted elements for style.
    this.styleWhitelist = new this.Set(['text-align', 'list-style-type', 'float']);
    this.classBlacklist = new this.Set(['MsoNormal', 'MsoTitle', 'MsoHeader', 'MsoFootnoteText',
        'Bullet1', 'Bullet2', 'Apple-span-style']);

    this.classFilter = function(value) {
        var classes = value.split(' ');
        var filtered = [];
        for (var i = 0; i < classes.length; i++) {
            var c = classes[i];
            if (c && !this.classBlacklist[c]) {
                filtered.push(c);
            }
        }
        return filtered.join(' ');
    };
    this._defaultCopyAttribute = function(name, htmlnode, xhtmlnode) {
        var val = htmlnode.getAttribute(name);
        if (val) xhtmlnode.setAttribute(name, val);
        return !!val;
    };
    // Set up filters for attributes.
    // Filters may return false if nothing was copied, true or
    // undefined if an attribute was copied.
    var filter = this;
    this.attrFilters = new function(validation, editor) {
        var attrs = validation.elements.attributes;
        for (var i=0; i < attrs.length; i++) {
            this[attrs[i]] = validation._defaultCopyAttribute;
        }
        this['class'] = function(name, htmlnode, xhtmlnode) {
            var val = htmlnode.getAttribute('class');
            if (val) val = validation.classFilter(val);
            if (val) xhtmlnode.setAttribute('class', val);
            return !!val;
        };
        // allow a * wildcard to make all attributes valid in the filter
        // note that this is pretty slow on IE
        this['*'] = function(name, htmlnode, xhtmlnode) {
            var res = false;
            var nodeName = filter._getTagName(htmlnode);
            var bad = filter.badTagAttributes[nodeName];
            for (var i=0; i < htmlnode.attributes.length; i++) {
                var attr = htmlnode.attributes[i];
                var name = attr.name;
                if (bad && bad.contains(name)) {
                    continue;
                };
                if (attr.specified) {
                    xhtmlnode.setAttribute(name, attr.value);
                    res = true;
                };
            };
            return res;
        };
        if (editor.getBrowserName()=="IE") {
            this['class'] = function(name, htmlnode, xhtmlnode) {
                var val = htmlnode.className;
                if (val) {
                    val = validation.classFilter(val); 
                    if (val) xhtmlnode.setAttribute('class', val);
                } else {
                    val = htmlnode.getAttribute("class");
                    if (val) val = validation.classFilter(val);                        
                    if (val) xhtmlnode.setAttribute('class', val);
                }
                return !!val;
            };
            this['http-equiv'] = function(name, htmlnode, xhtmlnode) {
                var val = htmlnode.httpEquiv;
                if (val) xhtmlnode.setAttribute('http-equiv', val);
                return !!val;
            };
            this['xml:lang'] = this['xml:space'] = function(name, htmlnode, xhtmlnode) {
                try {
                    var val = htmlnode.getAttribute(name);
                    if (val) xhtmlnode.setAttribute(name, val);
                    return !!val;
                } catch(e) {
                }
            };
        }
        this.alt = function(name, htmlnode, xhtmlnode) {
            var val = htmlnode.getAttribute(name);
            var ok = val || xhtmlnode.tagName=='img';
            if (ok) xhtmlnode.setAttribute(name, val);
            return ok;
        };
        this.rowspan = this.colspan = function(name, htmlnode, xhtmlnode) {
            var val = htmlnode.getAttribute(name);
            var ok = val && val != '1';
            if (ok) xhtmlnode.setAttribute(name, val);
            return ok;
        };
        this.style = function(name, htmlnode, xhtmlnode) {
            var val = htmlnode.style.cssText;
            if (val) {
                var styles = val.split(/; */);
                for (var i = styles.length; i >= 0; i--) if (styles[i]) {
                    var parts = /^([^:]+): *(.*)$/.exec(styles[i]);
                    var name = parts[1].toLowerCase();
                    if (validation.styleWhitelist[name]) {
                        styles[i] = name+': '+parts[2];
                    } else {
                        styles.splice(i,1); // delete
                    }
                }
                if (styles[styles.length-1]) styles.push('');
                val = styles.join('; ').strip();
            };
            if (val) xhtmlnode.setAttribute('style', val);
            return !!val;
        };
    }(this, editor);

    // Exclude unwanted tags.
    this.excludeTags(['center', 'meta', 'title']);

    if (editor.config && editor.config.htmlfilter) {
        this.filterStructure = editor.config.htmlfilter.filterstructure;
        
        var exclude = editor.config.htmlfilter;
        if (exclude.a) {
            this.excludeAttributes(exclude.a);
        }
        if (exclude.t) {
            this.excludeTags(exclude.t);
        }
        if (exclude.c) {
            var c = asList(exclude.c);
            for (var i = 0; i < c.length; i++) {
                this.excludeTagAttributes(c[i].t, c[i].a);
            }
        }
        if (exclude.xstyle) {
            var s = asList(exclude.xstyle);
            for (var i = 0; i < s.length; i++) {
                this.styleWhitelist[s[i]] = 1;
            }
        }
        if (exclude['class']) {
            var c = asList(exclude['class']);
            for (var i = 0; i < c.length; i++) {
                this.classBlacklist[c[i]] = 1;
            }
        }
    };

    // Copy all valid attributes from htmlnode to xhtmlnode.
    // Returns true if at least one attribute was copied.
    this._copyAttributes = function(htmlnode, xhtmlnode, valid) {
        var name;
        var res = false;
        if (valid.contains('*')) {
            // allow all attributes on this tag
            res = this.attrFilters['*'](name, htmlnode, xhtmlnode);
            if (res===undefined) res = true;
        } else {
            for (var i = 0; i < valid.length; i++) {
                name = valid[i];
                var filter = this.attrFilters[name];
                if (filter) {
                    var f = filter(name, htmlnode, xhtmlnode);
                    res|=(f||f===undefined);
                }
            };
        }
        return res;
    };
    this._xmlCopyAttr = function(srcnode, target) {
        var valid = this.tagAttributes[srcnode.nodeName];
        for (var i = 0; i < valid.length; i++) {
            var val = srcnode.getAttribute(valid[i]);
            if (val) {
                target.setAttribute(valid[i], val);
            }
        };
    };

    this._convertToSarissaNode = function(ownerdoc, htmlnode) {
        var root = this._convertNodes(ownerdoc, htmlnode, null, new this.Set(['html']));
        this._cleanupBr(ownerdoc, root);
        this._cleanupParas(ownerdoc, root);
        return root;
    };

    // Clean up a paragraph. Any direct child which is not allowed in
    // the paragraph is moved to the parent. This may involved
    // splitting the paragraph, or if it is at the beginning or end it
    // may simply mean moving it out of the paragraph.
    this._cleanupPara = function(ownerdoc, para) {
        var permitted = this.States.p;
        var nodes = [[]];
        var idx = 0;
        for (var child = para.firstChild; child; child = child.nextSibling) {
            var nn = child.nodeName.toLowerCase();
            if (permitted[nn] && (nn != 'img' || !(/\bcaptioned\b/i.test(child.getAttribute('class'))))) {
                nodes[idx].push(child);
            } else {
                if (nodes[idx].length) {
                    nodes.push(child);
                } else {
                    nodes[idx] = child;
                }
                nodes.push([]);
                idx = nodes.length-1;
            }
        }
        if (!nodes[idx].length) {
            nodes.splice(idx,1);
        };
        if (nodes.length > 0 && nodes[0] instanceof Array && !nodes[0].length) {
            nodes.splice(0,1);
        }
        if (nodes.length==0 || (nodes.length==1 && nodes[0] instanceof Array)) {
            return; /* No change */
        }
        /* Need to cleanup this paragraph */
        var parentnode = para.parentNode;
        for (var idx = 0; idx < nodes.length; idx++) {
            var n = nodes[idx];
            if (n instanceof Array) {
                var newp = ownerdoc.createElement('p');
                this._xmlCopyAttr(para, newp);
                for (var ln = n.length-1; ln >= 0; ln--) {
                    var nn = n[ln].nodeName.toLowerCase();
                    if (nn=='br' || (nn=='#text' && (/^\s*$/.test(n[ln].nodeValue)))) {
                        n.splice(ln,1);
                    } else { break; }
                }
                if (n.length==0) {
                    continue;
                }
                for (var j = 0; j < n.length; j++) {
                    newp.appendChild(n[j]);
                }
                n = newp;
            }
            parentnode.insertBefore(n,para);
        }
        parentnode.removeChild(para);
    };

    this._cleanupParas = function(ownerdoc, root) {
        var paras = root.getElementsByTagName('p');
        for (var i = paras.length-1; i >= 0; i--) {
            this._cleanupPara(ownerdoc, paras[i]);
        }
    };
    /* Cleanup br tags: br at top level is replaced by a paragraph,
     * br at end of p|div is dropped.
     */
    this._cleanupBr = function(ownerdoc, root) {
        var breaks = root.getElementsByTagName('br');
        // Iterate backwards: removeChild removes node from breaks.
        for (var i = breaks.length-1; i >= 0; i--) {
            var node = breaks[i];
            var parentNode = node.parentNode;
            if (parentNode.tagName=='body') {
                var p = ownerdoc.createElement('p');
                var prev = node.previousSibling;
                if (prev && prev.nodeType==3) {
                    p.appendChild(prev);
                }
                parentNode.insertBefore(p,node);
                parentNode.removeChild(node);
            } else if (!node.nextSibling && (/(p|div)\b/i.test(parentNode.nodeName) && !(node.previousSibling&&node.previousSibling.nodeName=='br'))) {
                parentNode.removeChild(node);
            }
        }
    };

    this._convertNodes = function(ownerdoc, htmlnode, xhtmlparent, permitted) {
        var parentnode = xhtmlparent;
        var nodename = this._getTagName(htmlnode);
        var nostructure = !this.filterstructure;

        // TODO: This permits valid tags anywhere. it should use the state
        // table in xhtmlvalid to only permit tags where the XHTML DTD
        // says they are valid.
        var validattrs = this.tagAttributes[nodename];
        if (validattrs && (nostructure || permitted[nodename])) {
            try {
                var xhtmlnode = ownerdoc.createElement(nodename);
                parentnode = xhtmlnode;
            } catch (e) { };
            
            if (validattrs && xhtmlnode) {
                if (!this._copyAttributes(htmlnode, xhtmlnode, validattrs) && nodename=='span') {
                    parentnode = xhtmlparent;
                    xhtmlnode = null;
                }
            }
        } else {
            // Stripping this tag, maybe we also want to strip the
            // content of the tag.
            if (this.nastyTags[nodename]) { return null; }
        }

        var kids = htmlnode.childNodes;
        var permittedChildren = this.States[parentnode.tagName] || permitted;

        if (kids.length == 0) {
            if (htmlnode.text && htmlnode.text != "" &&
                (nostructure || permittedChildren['#text'])) {
                var text = htmlnode.text;
                var tnode = ownerdoc.createTextNode(text);
                parentnode.appendChild(tnode);
            }
        } else {
            for (var i = 0; i < kids.length; i++) {
                var kid = kids[i];

                if (kid.parentNode !== htmlnode) {
                    if (kid.tagName.toLowerCase()=='body') {
                        if (nodename != 'html') continue;
                    } else if (kid.parentNode.tagName === htmlnode.tagName) {
                        continue; // IE bug: nodes appear multiple places
                    }
                }
                
                if (kid.nodeType == 1) {
                    var newkid = this._convertNodes(ownerdoc, kid, parentnode, permittedChildren);
                    if (newkid != null) {
                        parentnode.appendChild(newkid);
                    };
                } else if (kid.nodeType == 3) {
                    if (nostructure || permittedChildren['#text']) {
                        parentnode.appendChild(ownerdoc.createTextNode(kid.nodeValue));
                    }
                } else if (kid.nodeType == 4) {
                    if (nostructure || permittedChildren['#text']) {
                        parentnode.appendChild(ownerdoc.createCDATASection(kid.nodeValue));
                    }
                } else if (kid.nodeType == 8) {
                    parentnode.appendChild(ownerdoc.createComment(kid.nodeValue));
                }
            }
        } 
        return xhtmlnode;
    };
}


/* - kupuploneeditor.js - */
/*****************************************************************************
 *
 * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/
KupuEditor.prototype._getBase = function(dom) {
    var base = dom.getElementsByTagName('base');
    if (base.length) {
        return base[0].getAttribute('href');
    } else {
        return '';
    }
};

// $Id: kupuploneeditor.js 51158 2008-01-31 16:00:24Z duncan $
KupuEditor.prototype.makeLinksRelative = function(contents,base,debug) {
    // After extracting text from Internet Explorer, all the links in
    // the document are absolute.
    // we can't use the DOM to convert them to relative links, since
    // its the DOM that corrupts them to absolute to begin with.
    // Instead we can find the base from the DOM and do replace on the
    // text until all our links are relative.

    var href = base.replace(/\/[^\/]*$/, '/');
    var pageid = /.*\/([^\/]*)$/.exec(base)[1];
    var hrefparts = href.split('/');
    contents = contents.replace(/(<[^>]* (?:src|href)=")([^"]*)"/g,
        function(str, tag, url, offset, contents) {
        var resolveuid = url.indexOf('/resolveuid/');
        if (resolveuid != -1) {
            str = tag + url.substr(resolveuid+1)+'"';
            return str;
        }
        var urlparts = url.split('#');
        var anchor = urlparts[1] || '';
        url = urlparts[0];
        var urlparts = url.split('/');
        var common = 0;
        while (common < urlparts.length &&
            common < hrefparts.length &&
            urlparts[common]==hrefparts[common]) {
            common++;
        }
        var last = urlparts[common];
        if (common == urlparts.length) { urlparts[common] = '.'; }
        else if (common+1 == urlparts.length && (last=='emptypage'||last==pageid)) {
            urlparts[common] = '';
        }
            // The base and the url have 'common' parts in common.
            // First two are the protocol, so only do stuff if more
            // than two match.
        if (common > 2) {
            var path = [];
            var i = 0;
            for (; i+common < hrefparts.length-1; i++) {
                path[i] = '..';
            };
            while (common < urlparts.length) {
                path[i++] = urlparts[common++];
            };
            if (i==0 && !anchor) {
                path[i++] = '#';
            }
            str = path.join('/');
            if (anchor || str=="") {
                str = [str,anchor].join('#');
            }
            str = tag + str+'"';
        };
        return str;
    });
    // Remove empty links
    contents = contents.replace(/<a\s+href="[^"]*"\s*>\s*<\/a>/g, '');
    // Fixup empty paras.
    contents = contents.replace(/<((p|div)\b[^>]*)>\s*<\/\2>(<br \/>)*/g, '<$1>&nbsp;</$2>').strip();
    return contents;
};

KupuEditor.prototype.saveDataToField = function(form, field) {
    var sourcetool = this.getTool('sourceedittool');
    if (sourcetool) {sourcetool.cancelSourceMode();};

    if (!this._initialized) {
        return;
    };
    this._initialized = false;

    // set the window status so people can see we're actually saving
    window.status= "Please wait while saving document...";

    // pass the content through the filters
    this.logMessage("Starting HTML cleanup");

    var transform = this._filterContent(this.getInnerDocument().documentElement);

    // We need to get the contents of the body node as xml, but we don't
    // want the body node itself, so we use a regex to remove it
    var contents = this.getXMLBody(transform);
    if (/^<body[^>]*>(<\/?(p|br)[^>]*>|\&nbsp;|\s)*<\/body>$/.test(contents)) {
        contents = ''; /* Ignore nearly empty contents */
    }
    var base = this._getBase(transform);
    contents = this._fixupSingletons(contents);
    contents = this.makeLinksRelative(contents, base).replace(/<\/?body[^>]*>/g, "");
    this.logMessage("Cleanup done, sending document to server");

    // now create the form input
    field.value = contents;

    this.content_changed = false;
};


/* - kupuploneui.js - */
/*****************************************************************************
 *
 * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/
// This file deliberately left blank


/* - kupusourceedit.js - */
/*****************************************************************************
 *
 * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/
// $Id$


function SourceEditTool(sourcebuttonid, sourceareaid) {
    /* Source edit tool to edit document's html source */
    this.sourceButton = getFromSelector(sourcebuttonid);
    this.sourcemode = false;
    this._currently_editing = null;

    // method defined inline to support closure
    // XXX would be nice to have this defined on the prototype too, because
    // of subclassing issues?
    this.getSourceArea = function() {
        return getFromSelector(sourceareaid);
    };
};

SourceEditTool.prototype = new KupuTool;

SourceEditTool.prototype.cancelSourceMode = function() {
    if (this._currently_editing) {
        this.switchSourceEdit(null, true);
    }
};

SourceEditTool.prototype.updateState = 
        SourceEditTool.prototype.cancelSourceMode;

SourceEditTool.prototype.initialize = function(editor) {
    /* attach the event handlers */
    this.editor = editor;
    if (!this.sourceButton) return;
    addEventHandler(this.sourceButton, "click", this.switchSourceEdit, this);
    this.editor.logMessage(_('Source edit tool initialized'));
};

SourceEditTool.prototype.switchSourceEdit = function(event, nograb) {
    var kupu = this.editor;
    var docobj = this._currently_editing||kupu.getDocument();
    var editorframe = docobj.getEditable();
    var sourcearea = this.getSourceArea();
    var kupudoc = docobj.getDocument();
    var sourceClass = 'kupu-sourcemode';

    if (!this.sourcemode) {
        if (window.drawertool) {
            window.drawertool.closeDrawer();
        }
        if (/on/i.test(kupudoc.designMode)) {
            kupudoc.designMode = 'Off';
        };
        kupu._initialized = false;

        var data='';
        if(kupu.config.filtersourceedit) {
            window.status = _('Cleaning up HTML...');
            var transform = kupu._filterContent(kupu.getInnerDocument().documentElement);
            data = kupu.getXMLBody(transform);
            data = kupu._fixupSingletons(data).replace(/<\/?body[^>]*>/g, "");
            if (kupu._getBase && kupu.makeLinksRelative) {
                var base = kupu._getBase(transform);
                data = kupu.makeLinksRelative(data, base).replace(/<\/?body[^>]*>/g, "");
            };
            window.status = '';
        } else {
            data = kupu.getHTMLBody();
        }
        sourcearea.value = data.strip();
        kupu.setClass(sourceClass);
        editorframe.style.display = 'none';
        sourcearea.style.display = 'block';
        if (!nograb) {
            sourcearea.focus();
        };
        this._currently_editing = docobj;
      } else {
        kupu.setHTMLBody(sourcearea.value);
        kupu.clearClass(sourceClass);
        sourcearea.style.display = 'none';
        editorframe.style.display = 'block';
        if (/off/i.test(kupudoc.designMode)) {
            kupudoc.designMode = 'On';
        };
        if (!nograb) {
            docobj.getWindow().focus();
            var selection = this.editor.getSelection();
            selection.collapse();
        };

        kupu._initialized = true;
        this._currently_editing = null;
        this.editor.updateState();
    };
    this.sourcemode = !this.sourcemode;
};

SourceEditTool.prototype.enable = function() {
    kupuButtonEnable(this.sourceButton);
};

SourceEditTool.prototype.disable = function() {
    kupuButtonDisable(this.sourceButton);
};

function MultiSourceEditTool(sourcebuttonid, textareaprefix) {
    /* Source edit tool to edit document's html source */
    this.sourceButton = getFromSelector(sourcebuttonid);
    this.textareaprefix = textareaprefix;

    this._currently_editing = null;
};

MultiSourceEditTool.prototype = new SourceEditTool;

MultiSourceEditTool.prototype.getSourceArea = function() {
    var docobj = this._currently_editing||kupu.getDocument();
    var sourceareaid = this.textareaprefix + docobj.getEditable().id;
    return getFromSelector(sourceareaid);
};


/* - kupudrawers.js - */
/*****************************************************************************
 *
 * Copyright (c) 2003-2008 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/
// $Id: kupudrawers.js 65353 2009-05-22 07:29:45Z duncan $

function kupu_busy(ed) {
    if (ed.busy) ed.busy();
}
function kupu_notbusy(ed, force) {
    if (ed.notbusy) ed.notbusy(force);
}
function DrawerTool() {
    /* a tool to open and fill drawers
       this tool has to (and should!) only be instantiated once
    */
    this.drawers = {};
    this.current_drawer = null;

    this.initialize = function(editor) {
        this.editor = editor;
        this.isIE = this.editor.getBrowserName() == 'IE';
        // this essentially makes the drawertool a singleton
        window.drawertool = this;
    };

    this.registerDrawer = function(id, drawer, editor) {
        this.drawers[id] = drawer;
        drawer.initialize(editor || this.editor, this);
    };

    this.openDrawer = function(id, args) {
        /* open a drawer */
        if (this.current_drawer) {
            this.closeDrawer();
        };
        var drawer = this.drawers[id];
        if (this.isIE) {
            drawer.editor._saveSelection();
        }
        this.current_drawer = drawer;
        if (args===undefined) args = [];
        if (this.isIE) {
            drawer.initMask(drawer.element);
        }
        drawer.createContent.apply(drawer, args);
        drawer.editor.suspendEditing();
        drawer.fixMask();
    };

    this.updateState = function(selNode) {
    };

    this.closeDrawer = function(button) {
        if (!this.current_drawer) {
            return;
        };
        this.current_drawer.hide();
        this.current_drawer.editor.resumeEditing();
        this.current_drawer = null;
        kupu_notbusy(this.editor, true);
    };
};

DrawerTool.prototype = new KupuTool;

function Drawer(elementid, tool) {
    /* base prototype for drawers */

    this.element = getFromSelector(elementid);
    this.tool = tool;
}
var proto = Drawer.prototype;

proto.initialize = function(editor, drawertool) {
    this.editor = editor;
    this.drawertool = drawertool;
    // on a small screen, the appearance of the drawer could scroll the screen.
    // Here we stored the position, so that we can restore it for the user.
    this.scrollPosition = document.documentElement.scrollTop;

};

proto.createContent = function() {
    /* fill the drawer with some content */
    // here's where any intelligence and XSLT transformation and such
    // is done
    this.element.style.display = 'block';
    this.focusElement();
};

proto.hide = function() {
    if (this.maskframe) {
        this.maskframe.style.display='none';
    }
    this.element.style.display = 'none';
    this.focussed = false;
    document.documentElement.scrollTop = this.scrollPosition; // see initialize
};

proto.focusElement = function() {
    // IE can focus the drawer element, but Mozilla needs more help
    this.focussed = false;
    var iterator = new NodeIterator(this.element);
    var currnode = iterator.next();
    while (currnode) {
        if (currnode.tagName && (currnode.tagName.toUpperCase()=='BUTTON' ||
            (currnode.tagName.toUpperCase()=='INPUT' && !(/nofocus/.test(currnode.className)))
            )) {
            this.focussed = true;
            function focusit() {
                try {
                    currnode.focus();
                }catch(e){};
            }
            timer_instance.registerFunction(this, focusit, 100);
            return;
        }
        currnode = iterator.next();
    }
};

proto.initMask = function(el) {
    var e = (this.maskframe = document.getElementById('kupu-maskframe'));
    if (!this.maskframe) {
        e = this.maskframe = newElement('iframe',
            {'id':'kupu-maskframe','src':"javascript:false;", 'frameBorder':"0", 'scrolling':"no" });
        var style = e.style;
        style.display = 'none';
    }
    el.parentNode.insertBefore(e, el);
};

proto.fixMask = function() {
    var mask = this.maskframe;
    if (mask) {
        if (mask.parentNode != this.element.parentNode) {
            this.element.parentNode.insertBefore(mask, this.element);
        }
        // display the mask to hide SELECT boxes in IE
        var el = this.element;
        var st = mask.style;
        var st1 = el.style;
        st.top=st1.top;
        st.left=st1.left;
        st.width = el.offsetWidth+'px';
        st.height = el.offsetHeight+'px';
        st.left = (el.offsetLeft)+'px';
        st.position = 'absolute';
        st.display = '';
    }
};

proto.switchMode = function(event) {
    event = event || window.event;
    var target = event.currentTarget || event.srcElement;
    var el = target;
    while (!(/^li$/i.test(el.nodeName))) { el = el.parentNode; };
    var thistab = el;
    while (!(/^ul$/i.test(el.nodeName))) { el = el.parentNode; };
    var tabs = el.getElementsByTagName('li');
    for (var i = 0; i < tabs.length; i++) {
        var el = tabs[i];
        var cls = el.className.replace(/\s*selected/g, '');
        if (el===thistab) {
            this.panel.className = 'kupu-panels '+cls;
            cls += ' selected';
        }
        if (el.className != cls) {
            el.className = cls;
        }
    }
    if (this.fillList) this.fillList();
    this.fixMask();
    if (event.preventDefault) { event.preventDefault();}
    event.returnValue = false;
    return false;
};


function DrawerWithAnchors(editor, drawertool, anchorui) {
    Drawer.call(this, editor, drawertool);
    this.anchorui = anchorui;
    this.anchorframe = null;
}
DrawerWithAnchors.prototype = new Drawer;
proto = DrawerWithAnchors.prototype;

proto.initAnchors = function() {
    var limit = 40;
    var anchorframe = this.anchorframe;
    var ed = this.editor;
    function onloadEvent() {
        var state = anchorframe.readyState;
        if (state && !(/complete/.test(state))) {
            if (limit-- && anchorframe.src==src) {
                timer_instance.registerFunction(this, onloadEvent, 500);
            } else {
                kupu_notbusy(ed, true);
            }
            return;
        };
        if(window.drawertool && window.drawertool.current_drawer) {
            window.drawertool.current_drawer.anchorframe_loaded();
        };
        kupu_notbusy(ed);
    };

    var id = 'kupu-linkdrawer-anchors';
    var base = (this.anchorui = getBaseTagClass(this.element, 'div', id));
    if (base) {
        var inp = base.getElementsByTagName('input');
        if (inp.length > 1) {
            inp[1].disabled = true;
        }
        var src = inp[0].value;
        inp[0].value = "";
        if (!src) { return; }
        kupu_busy(ed);
        if (this.anchorframe.readyState) { // IE
            anchorframe.src = src;
            onloadEvent();
        } else { // FF
            this.anchorframe.onload = onloadEvent;
            anchorframe.src = src;
        }
    }
};
proto.anchorSelect = function() {
    return this.anchorui && this.anchorui.getElementsByTagName('select')[0];
};

proto.addSelectEvent = function() {
    var s = this.anchorSelect();
    if (s) {
        addEventHandler(s, 'change', this.selChange, this);
    }
};

proto.hideAnchors = function() {
    this.anchorui.style.display = 'none';
};

proto.anchorText = function(a) {
    // Text inside anchor, or immediate sibling block tag, or parent block.
    var blocktag = /^div|p|body|td|h.$/i;
    var txt = '';
    var prefix = '#' + a.name;

    for (var node = a; node && !txt; node=node.parentNode) {
        var txt = node.textContent || node.innerText || '';
        if (txt || blocktag.test(node.nodeName)) {
            break;
        }

        for (var sibling = node.nextSibling; sibling && !txt; sibling = sibling.nextSibling) {
            if (sibling.nodeName.toLowerCase()=='#text') {
                txt = sibling.data.strip();
            } else {
                txt += sibling.textContent || sibling.innerText ||'';
            };
            txt = txt.strip();
        }
    }
    if (txt) {
        txt = ' (' + (txt||'').substring(0,80).reduceWhitespace().strip()+')';
    }
    return prefix + txt;
};

proto.selChange = function() {};

proto.anchorframe_loaded = function() {
    this.showAnchors('');
};

proto.showAnchors = function(selected) {
    var select = this.anchorSelect();
    if (select == undefined) return;
    var opts = select.options;

    while (opts.length > 1) opts[1] = null;
    try {
        var doc = this.anchorframe.contentWindow.document;
        var anchors = doc.anchors;
    } catch(e) {
        this.hideAnchors();
        return;
    }
    for (var i = 0; i < anchors.length; i++) {
        var a = anchors[i];
        if (a.name) {
            var opt = document.createElement('option');
            opt.text = this.anchorText(anchors[i]);
            var v = anchors[i].name;
            opt.value = v;
            if (v==selected) opt.selected = true;
            select.options.add(opt);
        }
    }
    select.disabled = false;
    if (opts.length > 1) {
        this.anchorui.style.display = '';
    }
};

proto.getFragment = function() {
    var select = this.anchorSelect();
    if (select) {
        var anchor = select.options[select.selectedIndex].value;
        if (anchor) return '#' + anchor;
    }
    return '';
};

function LinkDrawer(elementid, tool) {
    /* Link drawer */
    DrawerWithAnchors.call(this, elementid, tool);

    var input = getBaseTagClass(this.element, 'input', 'kupu-linkdrawer-input');
    var embed = getBaseTagClass(this.element, 'textarea', 'kupu-embed-input');
    var preview = getBaseTagClass(this.element, 'iframe', 'kupu-linkdrawer-preview');
    var watermark = getBaseTagClass(this.element, 'div', 'watermark');
    this.anchorframe = preview;
    this.anchorui = getBaseTagClass(this.element, 'tr', 'kupu-linkdrawer-anchors');
    this.target = '';
    this.panel = getBaseTagClass(this.element, 'div', 'kupu-panels');
    var kuputabs = getBaseTagClass(this.element, 'ul', 'kupu-tabs');
    if (kuputabs) {
	var tabs = kuputabs.getElementsByTagName('a');
	for (var i = 0; i < tabs.length; i++) {
            addEventHandler(tabs[i], 'click', this.switchMode, this);
	}
    }
    if (embed) {
	addEventHandler(embed, 'click', function() { if(embed.defaultValue==embed.value) {embed.select();} });
    }

    this.selChange = function() {
        var anchor = this.getFragment();

        input.value = input.value.replace(/#[^#]*$/, '');
        if (anchor) {
            input.value += anchor;
        }
    };
    this.addSelectEvent();

    this.createContent = function() {
        /* display the drawer */
        var ed = this.editor;
        var currnode = ed.getSelectedNode();
        var linkel = ed.getNearestParentOfType(currnode, 'a');
        input.value = "";

        this.preview();
        if (linkel) {
            input.value = linkel.getAttribute('href');
        } else {
            input.value = 'http://';
        };
        var obj = ed.getNearestParentOfType(currnode, 'object') || ed.getNearestParentOfType(currnode, 'embed');
        if (obj) {
            embed.value = getOuterHtml(obj);
        } else {
            embed.value = embed.defaultValue;
        }
        this.element.style.display = 'block';
        this.hideAnchors();
        this.focusElement();
    };

    this.save = function() {
        /* add or modify a link */
        this.editor.resumeEditing();
        if (this.getMode()) {
            var url = input.value;
            this.tool.createLink(url, null, null, this.target, null, 'external-link');
            input.value = '';
        } else {
            // Import the html
            var doc = this.editor.getInnerDocument();
            var selection = this.editor.getSelection();
            var dummy = doc.createElement("div");
            dummy.innerHTML = embed.value;
            try {
                for (var j=dummy.childNodes.length-1; j >= 0; j--) {
                    var c = dummy.childNodes[j];
                    if (/^\//.test(c.nodeName))
                    {
                        dummy.removeChild(c);
                    }
                }
                while (dummy.firstChild) {
                    var c= dummy.firstChild;
                    selection.replaceWithNode(c, !c.nextSibling);
                };
            } catch(e) {};
        }
        // XXX when reediting a link, the drawer does not close for
        // some weird reason. BUG! Close the drawer manually until we
        // find a fix:
        this.drawertool.closeDrawer();
    };


    function currentAnchor() {
        var bits = input.value.split('#');
        var current = bits.length > 1 ? bits[bits.length-1] : '';
        return current;
    }

    this.getMode = function() {
        return !!(/addlink/.test(this.panel.className));
    };
    this.preview = function() {
        if (this.getMode()) {
            var ok = false;
            watermark.style.display='';
            if (/^http(s?):\x2f\x2f./.test(input.value)) {
                try {
                    preview.src = input.value;
                    ok = true;
                } catch(e) { alert('Preview blew up"'+input.value+'"');};
            } else {
                preview.src = '';
                if (input.value.strip()) {
                    alert(_('Can only preview web urls'));
                }
            }
            if (ok) {
                this.showAnchors(currentAnchor());
                if (this.editor.getBrowserName() == 'IE') {
                    preview.width = "800";
                    preview.height = "365";
                    preview.style.zoom = "60%";
                };
            }
        };
    };

    this.preview_loaded = function() {
        watermark.style.display = (/^http(s?):\x2f\x2f./.test(input.value))?'none':'';
        var here = input.value;
        try {
            var there = preview.contentWindow.location.href;
        } catch(e) { return; }

        if (there && here != there && !(/^about:/.test(there))) {
            input.value = there;
        }
        this.showAnchors(currentAnchor());
    };
    addEventHandler(preview, "load", this.preview_loaded, this);
};

LinkDrawer.prototype = new DrawerWithAnchors;

function TableDrawer(elementid, tool) {
    /* Table drawer */
    this.element = getFromSelector(elementid);
    this.tool = tool;

    this.addpanel = getBaseTagClass(this.element, 'div', 'kupu-tabledrawer-addtable');
    this.editpanel = getBaseTagClass(this.element, 'div', 'kupu-tabledrawer-edittable');
    var editclassselect = getBaseTagClass(this.element, 'select', 'kupu-tabledrawer-editclasschooser');
    var addclassselect = getBaseTagClass(this.element, 'select', 'kupu-tabledrawer-addclasschooser');
    var alignselect = getBaseTagClass(this.element, 'select', 'kupu-tabledrawer-alignchooser');
    var newrowsinput = getBaseTagClass(this.element, 'input', 'kupu-tabledrawer-newrows');
    var newcolsinput = getBaseTagClass(this.element, 'input', 'kupu-tabledrawer-newcols');
    var makeheadercheck = getBaseTagClass(this.element, 'input', 'kupu-tabledrawer-makeheader');

    this.createContent = function() {
        var editor = this.editor;
        var selNode = editor.getSelectedNode();

        function fixClasses(classselect) {
            if (editor.config.table_classes) {
                var classes = editor.config.table_classes['class'];
                while (classselect.hasChildNodes()) {
                    classselect.removeChild(classselect.firstChild);
                };
                for (var i=0; i < classes.length; i++) {
                    var classinfo = classes[i];
                    var caption = classinfo.xcaption || classinfo;
                    var classname = classinfo.classname || classinfo;

                    var option = document.createElement('option');
                    var content = document.createTextNode(caption);
                    option.appendChild(content);
                    option.setAttribute('value', classname);
                    classselect.appendChild(option);
                };
            };
        };
        fixClasses(addclassselect);
        fixClasses(editclassselect);

        var table = editor.getNearestParentOfType(selNode, 'table');
        var show, hide;
        if (!table) {
            // show add table drawer
            show = this.addpanel;
            hide = this.editpanel;
        } else {
            // show edit table drawer
            show = this.editpanel;
            hide = this.addpanel;
            var align = this.tool._getColumnAlign(selNode);
            selectSelectItem(alignselect, align);
            selectSelectItem(editclassselect, table.className);
        };
        hide.style.display = 'none';
        show.style.display = 'block';
        this.element.style.display = 'block';
        this.focusElement();
    };

    this.createTable = function() {
        this.editor.resumeEditing();
        var rows = newrowsinput.value;
        var cols = newcolsinput.value;
        var style = addclassselect.value;
        var add_header = makeheadercheck.checked;
        this.tool.createTable(parseInt(rows), parseInt(cols), add_header, style);
        this.drawertool.closeDrawer();
    };

    this.delTableRow = function() {
        this.editor.resumeEditing();
        this.tool.delTableRow();
        this.editor.suspendEditing();
    };

    this.addTableRow = function() {
        this.editor.resumeEditing();
        this.tool.addTableRow();
        this.editor.suspendEditing();
    };

    this.delTableColumn = function() {
        this.editor.resumeEditing();
        this.tool.delTableColumn();
        this.editor.suspendEditing();
    };

    this.addTableColumn = function() {
        this.editor.resumeEditing();
        this.tool.addTableColumn();
        this.editor.suspendEditing();
    };

    this.fixTable = function() {
        this.editor.resumeEditing();
        this.tool.fixTable();
        this.editor.suspendEditing();
    };

    this.fixAllTables = function() {
        this.editor.resumeEditing();
        this.tool.fixAllTables();
        this.editor.suspendEditing();
    };

    this.delTable = function() {
        this.editor.resumeEditing();
        this.tool.delTable();
        this.drawertool.closeDrawer();
    };

    this.setTableClass = function(className) {
        this.editor.resumeEditing();
        this.tool.setTableClass(className);
        this.editor.suspendEditing();
    };

    this.setColumnAlign = function(align) {
        this.editor.resumeEditing();
        this.tool.setColumnAlign(align);
        this.editor.suspendEditing();
    };
};

TableDrawer.prototype = new Drawer;

function LibraryDrawer(tool, xsluri, libsuri, searchuri, baseelement, selecturi) {
    /* a drawer that loads XSLT and XML from the server
       and converts the XML to XHTML for the drawer using the XSLT

       there are 2 types of XML file loaded from the server: the first
       contains a list of 'libraries', partitions for the data items,
       and the second a list of data items for a certain library

       all XML loading is done async, since sync loading can freeze Mozilla
    */
    this.showupload = '';
    this.showanchors = '';
    this.multiple = false;
    this.currentSelection = [];

    this.init = function(tool, xsluri, libsuri, searchuri, baseelement, selecturi) {
        /* This method is there to thin out the constructor and to be
           able to inherit it in sub-prototypes. Don't confuse this
           method with the component initializer (initialize()).
        */
        // these are used in the XSLT. Maybe they should be
        // parameterized or something, but we depend on so many other
        // things implicitly anyway...
        this.drawerid = 'kupu-librarydrawer';
        this.librariespanelid = 'kupu-librariespanel';
        this.resourcespanelid = 'kupu-resourcespanel';
        this.propertiespanelid = 'kupu-propertiespanel';
        this.breadcrumbsid = 'kupu-breadcrumbs';

        if (baseelement) {
            this.baseelement = getFromSelector(baseelement);
        } else {
            this.baseelement = getBaseTagClass(document.body, 'div', 'kupu-librarydrawer-parent');
        }
        this.anchorframe = getBaseTagClass(this.baseelement, 'iframe', 'kupu-anchorframe');
        var e;
        this.tool = tool;
        this.element = document.getElementById(this.drawerid);
        if (!this.element) {
            e = document.createElement('div');
            e.id = this.drawerid;
            e.className = 'kupu-drawer '+this.drawerid;
            this.baseelement.appendChild(e);
            this.element = e;
        }
        this.shared.xsluri = xsluri;
        this.libsuri = libsuri;
        this.searchuri = searchuri;
        this.selecturi = selecturi;

        // marker that gets set when a new image has been uploaded
        this.shared.newimages = null;

        // the following vars will be available after this.initialize()
        // has been called

        // this will be filled by this._libXslCallback()
        this.shared.xsl = null;
        // this will be filled by this.loadLibraries(), which is called
        // somewhere further down the chain starting with
        // this._libsXslCallback()
        this.xmldata = null;

    };
    if (tool) {
        this.init(tool, xsluri, libsuri, searchuri, baseelement, selecturi);
    }

    this.initialize = function(editor, drawertool) {
        this.editor = editor;
        this.drawertool = drawertool;

        // load the xsl and the initial xml
        this._loadXML(this.shared.xsluri, this._libsXslCallback);
    };

    this.hide = function() {
        var el = this.element;
        el.style.left = el.style.top = '';
        LibraryDrawer.prototype.hide.call(this);
    };




    /*** bootstrapping ***/

    this._libsXslCallback = function(dom) {
        /* callback for when the xsl for the libs is loaded

            this is called on init and since the initial libs need
            to be loaded as well (and everything is async with callbacks
            so there's no way to wait until the XSL is loaded) this
            will also make the first loadLibraries call
        */
        this.shared.xsl = dom;
        Sarissa.getDomDocument(); /* Work round Sarissa initialisation glitch */

        // Change by Paul to have cached xslt transformers for reuse of
        // multiple transforms and also xslt params
        try {
            var xsltproc =  new XSLTProcessor();
            this.shared.xsltproc = xsltproc;
            xsltproc.importStylesheet(dom);
            xsltproc.setParameter("", "ie", this.editor.getBrowserName() == 'IE');
            xsltproc.setParameter("", "drawertype", this.drawertype);
            xsltproc.setParameter("", "drawertitle", this.drawertitle);
            xsltproc.setParameter("", "showupload", this.showupload);
            xsltproc.setParameter("", "showanchors", this.showanchors);
            if (this.target !== undefined) {
                xsltproc.setParameter("", "link_target", this.target);
            }
            if (this.editor.config && !!this.editor.config.captions) {
                xsltproc.setParameter("", "usecaptions", 'yes');
            }
        } catch(e) {
            if (e && e.name && e.message) e = e.name+': '+e.message;
            alert("xlstproc error:" + e);
            return; // No XSLT Processor, maybe IE 5.5?
        }
        if (this.xmldata) {
            this.updateDisplay(this.drawerid);
        };
    };

    this.setTitle = function(t) {
        this.drawertitle = t;
        var xsltproc = this.shared.xsltproc;
        if (xsltproc) {
            xsltproc.setParameter("", "drawertitle", this.drawertitle);
        };
    };

    this.createContent = function() {
        this.removeSelection();
        // Make sure the drawer XML is in the current Kupu instance
        if (this.element.parentNode != this.baseelement) {
            this.baseelement.appendChild(this.element);
        }
        // load the initial XML
        if(!this.xmldata) {
            // Do a meaningful test to see if this is IE5.5 or some other
            // editor-enabled version whose XML support isn't good enough
            // for the drawers
            if (!window.XSLTProcessor) {
               alert("This function requires better XML support in your browser.");
               return;
            }
            this.loadLibraries();
        } else {
            var libraries = this.xmldata.selectSingleNode("/libraries");
            var old = libraries.selectSingleNode("library[@id='kupu-current-selection']");
            if (old) {
                libraries.removeChild(old);
            }
            if (this.shared.newimages) {
                this.reloadCurrent();
                this.shared.newimages = null;
            };
            this.updateDisplay();
            this.initialSelection();
        };

        // display the drawer div
        this.element.style.display = 'block';
    };

    this._singleLibsXslCallback = function(dom) {
        /* callback for then the xsl for single libs (items) is loaded

            nothing special needs to be called here, since initially the
            items pane will be empty
        */
        this.singlelibxsl = dom;
    };

    this.loadLibraries = function() {
        /* load the libraries and display them in a redrawn drawer */
        this._loadXML(this.libsuri, this._libsContentCallback);
    };

    this._libsContentCallback = function(dom) {
        /* this is called when the libs xml is loaded

            does the xslt transformation to set up or renew the drawer's full
            content and adds the content to the drawer
        */
        this.xmldata = dom;
        this.xmldata.setProperty("SelectionLanguage", "XPath");

        // replace whatever is in there with our stuff
        this.updateDisplay(this.drawerid);
        this.initialSelection();
    };

    this.initialSelection = function() {
        if (this.selectedSrc && this.selecturi) {
            this.selectCurrent();
            return;
        }
        var libnode_path = '/libraries/library[@selected]';
        var libnode = this.xmldata.selectSingleNode(libnode_path);
        if (libnode) {
            var id = libnode.getAttribute('id');
            this.selectLibrary(id);
        }
    };

    this.updateDisplay = function(id) {
      /* (re-)transform XML and (re-)display the necessary part
       */
        if(!id) {
            id = this.drawerid;
        };

        var xsltproc = this.shared.xsltproc;
        if (!xsltproc) {
            return;
        }
        for (var k in this.options) {
            xsltproc.setParameter("", k, this.options[k]);
        }
        xsltproc.setParameter("", "multiple", this.multiple ? "yes" : "");
        xsltproc.setParameter("", "showupload", this.showupload ? "yes" : "");
        xsltproc.setParameter("", "showanchors", this.showanchors);

        var doc = this._transformXml();
        var sourcenode = doc.selectSingleNode('//*[@id="'+id+'"]');
        var targetnode = document.getElementById(id);
        if (!sourcenode || !targetnode) return;

        var cls = sourcenode.getAttribute('class');
        if (cls) {
            targetnode.className = cls;
        }
        Sarissa.copyChildNodes(sourcenode, targetnode);
        if (!this.focussed) {
            this.focusElement();
        }
        var el = document.getElementById('kupu-preview-image');
        if (el && el.width=='1') {
            kupuFixImage(el);
        }
        // Mark drawer as having a selection or not
        var el = this.element;
        el.className = el.className.replace(' kupu-has-selection', '');
        if (this.xmldata.selectSingleNode("//*[@selected]//*[@checked]")) {
            this.element.className += ' kupu-has-selection';
        };

        if (this.editor.getBrowserName() == 'IE' && id == this.resourcespanelid) {
            this.updateDisplay(this.drawerid);
        };
        this.fixMask();
    };

    this.updateResources = function() {
        if (this.editor.getBrowserName() == 'IE') {
            this.updateDisplay(this.drawerid);
        } else {
            this.updateDisplay(this.breadcrumbsid);
            this.updateDisplay(this.resourcespanelid);
            this.updateDisplay(this.propertiespanelid);
        }
    };

    this.deselectActiveCollection = function() {
        var librariespanel = document.getElementById(this.librariespanelid);
        if (!librariespanel) return;

        var divs = librariespanel.getElementsByTagName('div');
        for (var i = 0; i < divs.length; i++) {
            var div = divs[i];
            div.className = div.className.replace(/[ -]*selected/,'');
        }
        /* Deselect the currently active collection or library */
        var selected;
        while ((selected = this.xmldata.selectSingleNode('//*[@selected]'))) {
            // deselect selected DOM node
            selected.removeAttribute('selected');
        };
    };

    /*** Load a library ***/

    this.selectLibrary = function(id) {
        /* unselect the currently selected lib and select a new one

            the selected lib (libraries pane) will have a specific CSS class
            (selected)
        */
        // remove selection in the DOM
        this.deselectActiveCollection();
        // as well as visual selection in CSS
        // XXX this is slow, but we can't do XPath, unfortunately
        var divs = this.element.getElementsByTagName('div');
        for (var i=0; i<divs.length; i++ ) {
          if (divs[i].className == 'kupu-libsource-selected') {
            divs[i].className = 'kupu-libsource';
          };
        };

        var libnode_path = '/libraries/library[@id="' + id + '"]';
        var libnode = this.xmldata.selectSingleNode(libnode_path);
        libnode.setAttribute('selected', '1');

        var items_xpath = "items";
        var items_node = libnode.selectSingleNode(items_xpath);

        if (items_node && !this.shared.newimages) {
            // The library has already been loaded before or was
            // already provided with an items list. No need to do
            // anything except for displaying the contents in the
            // middle pane. Newimages is set if we've lately
            // added an image.
            this.useCollection(libnode);
        } else {
            // We have to load the library from XML first.
            var src_uri = libnode.selectSingleNode('src/text()').nodeValue;
            src_uri = src_uri.strip(); // needs kupuhelpers.js
            // Now load the library into the items pane. Since we have
            // to load the XML, do this via a call back
            this._loadXML(src_uri, this._libraryContentCallback, null, false, libnode);
            this.shared.newimages = null;
        };
    };
    this.flagSelectedLib = function(id) {
        // instead of running the full transformations again we get a
        // reference to the element and set the classname...
        var newseldiv = document.getElementById(id);
        if (newseldiv) {
            newseldiv.className = 'kupu-libsource-selected';
        }
    };

    this._libraryContentCallback = function(dom, src_uri, libnode) {
        /* callback for when a library's contents (item list) is loaded

        This is also used as he handler for reloading a standard
        collection.
        */
        //var libnode = this.xmldata.selectSingleNode('//*[@selected]');
        var itemsnode = libnode.selectSingleNode("items");
        var bcnode = libnode.selectSingleNode("breadcrumbs");
        var newitemsnode = dom.selectSingleNode("//items");
        var newbc = dom.selectSingleNode("//breadcrumbs");

        // IE does not support importNode on XML document nodes. As an
        // evil hack, clone the node instead.

        if (this.editor.getBrowserName() == 'IE') {
            newitemsnode = newitemsnode.cloneNode(true);
            if (newbc) newbc = newbc.cloneNode(true);
        } else {
            newitemsnode = this.xmldata.importNode(newitemsnode, true);
            if (newbc) newbc = this.xmldata.importNode(newbc, true);
        }
        if (newbc) {
            if (bcnode) {
                libnode.replaceChild(newbc, bcnode);
            } else {
                libnode.appendChild(newbc);
            };
        };
        if (!itemsnode) {
            // We're loading this for the first time
            libnode.appendChild(newitemsnode);
        } else {
            // User has clicked reload
            libnode.replaceChild(newitemsnode, itemsnode);
        };
        this.useCollection(libnode);
    };

    this.selectBreadcrumb = function(item) {
        var src_uri = item.href;
        if (/\$src\$$/.test(src_uri)) {
            var target = this.xmldata.selectSingleNode('//resource[@selected]/uri/text()');
            if (target) {
                target = target.nodeValue.strip();
                src_uri = src_uri.replace(/\$src\$/, encodeURIComponent(target));
            } else {
                return false;
            };
        };
        this.deselectActiveCollection();
        this.removeSelection();

        // Case 2: We've already loaded the data, but there hasn't
        // been a reference made yet. So, make one :)

        var collnode_path = "/libraries/*[src/text()='" + src_uri + "']";
        var collnode = this.xmldata.selectSingleNode(collnode_path);
        if (collnode) {
            var items_node = collnode.selectSingleNode("items");
            if (items_node) {
                collnode.setAttribute('selected', '1');
                this.useCollection(collnode);
                return;
            }
        };

        // Case 3: We've not loaded the data yet, so we need to load it
        this._loadXML(src_uri, this._collectionContentCallback, null);
        return false;
    };

    this.useCollection = function(collnode) {
        if (this.currentSelection) {
            var leafnodes = collnode.selectNodes("//*[@checked]");
            for (var j = 0; j < leafnodes.length; j++) {
                leafnodes[j].removeAttribute('checked');
            };
            var sel = this.currentSelection;
            for (var i = 0; i < sel.length; i++) {
                var leafnodes = collnode.selectNodes("//*[@id='"+sel[i]+"']");
                for (var j = 0; j < leafnodes.length; j++) {
                    leafnodes[j].setAttribute('checked', '1');
                    if (!this.multiple) {
                        leafnodes[j].setAttribute('selected', '1');
                    };
                };
            };
        };
        collnode.setAttribute('selected', '1');
        this.flagSelectedLib(collnode.getAttribute('id'));
        this.updateResources();
    };
    /*** Load a collection ***/
    this.selectCollection = function(item, tag) {
        var id = item.id;

        tag = tag || 'collection';
        this.deselectActiveCollection();

        // First turn off current selection, if any
        this.removeSelection();

        var leafnode_path = "//"+tag+"[@id='" + id + "']";
        var leafnode = this.xmldata.selectSingleNode(leafnode_path);

        // Case 1: We've already loaded the data, so we just need to
        // refer to the data by id.
        var loadedInNode = leafnode.getAttribute('loadedInNode');
        if (loadedInNode) {
            var collnode_path = "/libraries/*[@id='" + loadedInNode + "']";
            var collnode = this.xmldata.selectSingleNode(collnode_path);
            if (collnode) {
                this.useCollection(collnode);
                return;
            };
        };

        // Case 2: We've already loaded the data, but there hasn't
        // been a reference made yet. So, make one :)
        var src_uri = leafnode.selectSingleNode('src/text()').nodeValue.strip();
        var collnode_path = "/libraries/*[src/text()='" + src_uri + "'][items]";
        var collnode = this.xmldata.selectSingleNode(collnode_path);
        if (collnode) {
            id = collnode.getAttribute('id');
            leafnode.setAttribute('loadedInNode', id);
            this.useCollection(collnode);
            return;
        };

        // Case 3: We've not loaded the data yet, so we need to load it
        // this is just so we can find the leafnode much easier in the
        // callback.
        leafnode.setAttribute('selected', '1');
        src_uri = leafnode.selectSingleNode('src/text()').nodeValue.strip();
        this._loadXML(src_uri, this._collectionContentCallback, null);
    };

    this._collectionContentCallback = function(dom, src_uri) {
        // Unlike with libraries, we don't have to find a node to hook
        // our results into (UNLESS we've hit the reload button, but
        // that is handled in _libraryContentCallback anyway).
        // We need to give the newly retrieved data a unique ID, we
        // just use the time.
        var date = new Date();
        var time = date.getTime();

        // attach 'loadedInNode' attribute to leaf node so Case 1
        // applies next time.
        var leafnode = this.xmldata.selectSingleNode('//*[@selected]');
        if (leafnode) {
            leafnode.setAttribute('loadedInNode', time);
        }
        this.deselectActiveCollection();

        var collnode = dom.selectSingleNode('/collection');
        collnode.setAttribute('id', time);
        collnode.setAttribute('selected', '1');

        var libraries = this.xmldata.selectSingleNode('/libraries');

        // IE does not support importNode on XML document nodes
        if (this.editor.getBrowserName() == 'IE') {
            collnode = collnode.cloneNode(true);
        } else {
            collnode = this.xmldata.importNode(collnode, true);
        }
        libraries.appendChild(collnode);
        this.useCollection(collnode);
    };

    /*** Reloading a collection or library ***/

    this.reloadCurrent = function() {
        // Reload current collection or library
        this.showupload = '';
        var current = this.xmldata.selectSingleNode('//*[@selected]');
        // make sure we're dealing with a collection even though a
        // resource might be selected
        if (current.tagName == "resource") {
            current.removeAttribute("selected");
            current = current.parentNode;
            current.setAttribute("selected", "1");
        };
        var src_node = current.selectSingleNode('src');
        if (!src_node) {
            // simply do nothing if the library cannot be reloaded. This
            // is currently the case w/ search result libraries.
            return;
        };

        var src_uri = src_node.selectSingleNode('text()').nodeValue;
        src_uri = src_uri.strip(); // needs kupuhelpers.js
        this._loadXML(src_uri, this._libraryContentCallback, null, true, current);
    };

    this.removeSelection = function() {
        // Mark the drawer as having no selection
        if (!this.xmldata) return;

        if (!this.multiple) {
            var items = this.xmldata.selectNodes('//resource[@checked]');
            for (var i = 0; i < items.length; i++) {
                items[i].removeAttribute('checked');
            };
        };

        // turn off current selection, if any
        var oldselxpath = '//resource[@selected]';
        var oldselitems = this.xmldata.selectNodes(oldselxpath);
        for (var i = 0; i < oldselitems.length; i++) {
            oldselitems[i].removeAttribute("selected");
            var id = oldselitems[i].getAttribute('id');

            var item = document.getElementById(id);
            if (item) {
                var spans = item.getElementsByTagName('span');
                for (var j = 0; j < spans.length; j++) {
                    var p = spans[j].parentNode;
                    p.className = p.className.replace(/(\s+|^)selected-item/, '');
                }
            }
        }
        this.showupload = '';
    };

    this.selectUpload = function() {
        this.removeSelection();
        this.showupload = 'yes';
        this.updateResources();
    };
    /*** Selecting a resource ***/

    this.selectItem = function (item, event) {
        var id = item.id;
        var newselxpath = '/libraries/*[@selected]//resource[@id="' + id + '"]';
        var src = this.xmldata.selectSingleNode(newselxpath+'/src');
        if (src) {
            event = event || window.event;
            if (event) {
                var target = event.target || event.srcElement;
            }
            if (target.nodeName.toLowerCase()!='input') {
                this.selectCollection(item, 'resource');
                return;
            }
        }

        /* select an item in the item pane, show the item's metadata */

        // First turn off current selection, if any
        this.removeSelection();

        // Grab XML DOM node for clicked "resource" and mark it selected
        var newselitem = this.xmldata.selectSingleNode(newselxpath);
        newselitem.setAttribute("selected", "1");

        var check = true;
        if (this.multiple) {
            if (newselitem.getAttribute('checked')) {
                check = false;
                var sel = this.currentSelection;
                for (var i=0; i < sel.length; i++) {
                    if (sel[i]==id) {
                        sel.splice(i, 1);
                        break;
                    };
                };
            } else {
                this.currentSelection.push(id);
            };
        } else {
            this.currentSelection = [id];
        };
        if (check) {
            newselitem.setAttribute('checked','1');
        } else {
            newselitem.removeAttribute('checked');
        };
        this.updateDisplay(this.propertiespanelid);

        // Don't want to reload the resource panel xml as it scrolls to
        // the top.
        var span = item.getElementsByTagName('span');
        if (span.length > 0) {
            span = span[0];
            var p = span.parentNode;
            p.className += ' selected-item';
            var inp = p.getElementsByTagName('input');
            if (inp) inp[0].checked = check;
        }

        if (this.editor.getBrowserName() == 'IE') {
            var ppanel = document.getElementById(this.propertiespanelid);
            var height = ppanel.clientHeight;
            if (height > ppanel.scrollHeight) height = ppanel.scrollHeight;
            if (height < 260) height = 260;
            document.getElementById(this.resourcespanelid).style.height = height+'px';
        }
        return;
    };


    this.search = function() {
        /* search */
        this.removeSelection();
        var searchvalue = getFromSelector('kupu-searchbox-input').value;
        //XXX make search variable configurable
        var body = 'SearchableText=' + encodeURIComponent(searchvalue);

        // the search uri might contain query parameters in HTTP GET
        // style. We want to do a POST though, so find any possible
        // parameters, trim them from the URI and append them to the
        // POST body instead.
        var chunks = this.searchuri.split('?');
        var searchuri = chunks[0];
        if (chunks[1]) {
            body += "&" + chunks[1];
        };
        this._loadXML(searchuri, this._searchCallback, body);
    };

    this._searchCallback = function(dom) {
        var resultlib = dom.selectSingleNode("/library");

        var items = resultlib.selectNodes("items/*");
        if (!items.length) {
            alert("No results found.");
            return;
        };

        // we need to give the newly retrieved data a unique ID, we
        // just use the time.
        var date = new Date();
        var time = date.getTime();
        resultlib.setAttribute("id", time);

        // deselect the previous collection and mark the result
        // library as selected
        this.deselectActiveCollection();
        resultlib.setAttribute("selected", "1");

        // now hook the result library into our DOM
        if (this.editor.getBrowserName() == 'IE') {
            resultlib = resultlib.cloneNode(true);
        } else {
            resultlib = this.xmldata.importNode(resultlib, true);
        }
        var libraries = this.xmldata.selectSingleNode("/libraries");
        libraries.appendChild(resultlib);

        this.updateDisplay(this.drawerid);
        var newseldiv = getFromSelector(time);
        newseldiv.className = 'kupu-libsource-selected';
    };

    this.selectCurrent = function() {
        var src = this.selectedSrc;
        var body = 'src=' + encodeURIComponent(src);

        // the uri might contain query parameters in HTTP GET
        // style. We want to do a POST though, so find any possible
        // parameters, trim them from the URI and append them to the
        // POST body instead.
        var chunks = this.selecturi.split('?');
        var uri = chunks[0];
        if (chunks[1]) {
            body += "&" + chunks[1];
        };
        this._loadXML(uri, this._selectedCallback, body);
    };

    this._selectedCallback = function(dom) {
        var resultlib = dom.selectSingleNode("/library");
        var id = "kupu-current-selection";
        resultlib.setAttribute("id", id);
        var leafnodes = resultlib.selectNodes("//resource");
        this.currentSelection = [];
        for (var i = 0; i < leafnodes.length; i++) {
            this.currentSelection.push(leafnodes[i].getAttribute('id'));
        };
        // deselect the previous collection and mark the result
        // library as selected
        this.deselectActiveCollection();
        resultlib.setAttribute("selected", "1");

        // now hook the result library into our DOM
        if (this.editor.getBrowserName() == 'IE') {
            resultlib = resultlib.cloneNode(true);
        } else {
            resultlib = this.xmldata.importNode(resultlib, true);
        }
        var libraries = this.xmldata.selectSingleNode("/libraries");
        libraries.appendChild(resultlib);
        this.useCollection(resultlib);
        this.updateDisplay(this.librariespanelid);
        this.flagSelectedLib(id);
        this.updateDisplay(this.breadcrumbsid);
    };

    this.save = function() {
        /* save the element, should be implemented on subclasses */
        throw "Not yet implemented";
    };

    /*** Auxiliary methods ***/

    this._transformXml = function() {
        /* transform this.xmldata to HTML using this.shared.xsl and return it */
	var result = this.shared.xsltproc.transformToDocument(this.xmldata);
        return result;
    };

    this._loadXML = function(uri, callback, body, reload, extra) {
        function _sarissaCallback() {
        /* callback for Sarissa
            when the callback is called because the data's ready it
            will get the responseXML DOM and call user_callback
            with the DOM as the first argument and the uri loaded
            as the second

            note that this method should be called in the context of an
            xmlhttp object
        */
            if (xmlhttp.readyState == 4) {
                if (xmlhttp.status && xmlhttp.status != 200) {
                    var errmessage = 'Error '+xmlhttp.status+' loading '+(uri||'XML');
                    kupu_notbusy(ed, true);
                    alert(errmessage);
                    throw "Error loading XML";
                };
                var dom = xmlhttp.responseXML;
                if (!dom || !dom.documentElement) { /* IE bug! */
                    dom = Sarissa.getDomDocument();
                    dom.loadXML(xmlhttp.responseText);
                }
                callback.apply(self, [dom, uri, extra]);
                kupu_notbusy(ed);
            };
        };
        var self = this;
        var ed = this.editor;
        /* load the XML from a uri
           calls callback with one arg (the XML DOM) when done
           the (optional) body arg should contain the body for the request
         */
	var xmlhttp = new XMLHttpRequest();
        var method = body?'POST':'GET';
        // be sure that body is null and not an empty string or
        // something
        body=body?body:null;

        kupu_busy(ed);
        try {
            xmlhttp.open(method, uri, true);
            xmlhttp.onreadystatechange = _sarissaCallback;
            if (method == "POST") {
                // by default, we would send a 'text/xml' request, which
                // is a dirty lie; explicitly set the content type to what
                // a web server expects from a POST.
                xmlhttp.setRequestHeader('content-type', 'application/x-www-form-urlencoded');
            };
            if (reload) {
                xmlhttp.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT");
            }
            xmlhttp.send(body);
        } catch(e) {
            if (e && e.name && e.message) { // Microsoft
                e = e.name + ': ' + e.message;
            }
            kupu_notbusy(ed, true);
            alert(e);
        }
    };

};

LibraryDrawer.prototype = new DrawerWithAnchors;
LibraryDrawer.prototype.shared = {}; // Shared data

function ImageLibraryDrawer(tool, xsluri, libsuri, searchuri, baseelement, selecturi) {
    /* a specific LibraryDrawer for images */

    this.drawertitle = "Insert Image";
    this.drawertype = "image";

    if (tool) {
        this.init(tool, xsluri, libsuri, searchuri, baseelement, selecturi);
    }

    this.createContent = function() {
        function getSel(sel, p, t) {
            var nodes = p.getElementsByTagName(t);
            for (var i = 0; i < nodes.length; i++) {
                if (sel.containsNode(nodes[i])) {
                    return nodes[i];
                };
            };
        }
        var ed = this.editor;
        var sel = ed.getSelection();
        var currnode = ed.getSelectedNode();
        var currimg = ed.getNearestParentOfType(currnode, 'OBJECT') || ed.getNearestParentOfType(currnode, 'IMG') ||
                      getSel(sel, currnode, 'object') || getSel(sel, currnode, 'img');
        this.selectedSrc = currimg?(currimg.data||currimg.src||null):null;
        this.options = {};
        if (currimg) {
            ed.getSelection().selectNodeContents(currimg);
            var className = currimg.className;
            var align = /\bimage-(left|right|inline)\b/.exec(className);
            if (align && align.length > 1) {
                this.options['image-align'] = align[1];
            };
            this.options['image-caption'] = /\bcaptioned\b/.test(className);
            this.options['image-class'] = className.replace(/\b(image-(left|right|inline)|captioned)\b/g,'').strip();
        }
        ImageLibraryDrawer.prototype.createContent.call(this);
    };

    // upload, on submit/insert press
    this.uploadImage = function() {
        var form = document.getElementById('kupu_upload_form');
        if (!form || (form.node_prop_image && form.node_prop_image.value == '')) {
            return;
        }

        if (form.node_prop_title && form.node_prop_title.value == "") {
            alert("Please enter a title for the image you are uploading");
            return;
        };
        this.upload_title = form.node_prop_title ? form.node_prop_title.value : '';
        if (form.node_prop_desc) {
            form.node_prop_desc.value = form.node_prop_desc.value.replace(/^\xa0|\xa0$/g,'');
        }
        form.submit();
    };

    // called for example when no permission to upload for some reason
    this.cancelUpload = function(msg) {
        var s = this.xmldata.selectSingleNode('/libraries/*[@selected]');
        s.removeAttribute("selected");
        this.updateDisplay();
        if (msg != '') {
            alert(msg);
        };
    };

    // called by onLoad within document sent by server
    this.finishUpload = function(uri, media, width, height) {
        this.editor.resumeEditing();
        var sizeselector = document.getElementsByName('image-size-selector');
        if (sizeselector && sizeselector.length > 0) {
            sizeselector = sizeselector[0];
            var index = sizeselector.selectedIndex;
            if (sizeselector.length > 0 && index >= 0) {
                uri += sizeselector.options[index].value;
            }
        }
        var radios = document.getElementsByName('image-align');
        var imgclass = "";
        for (var i = 0; i < radios.length; i++) {
            if (radios[i].checked) {
                imgclass = radios[i].value;
            };
        };
        var caption = document.getElementsByName('image-caption');
        if (caption && caption.length>0 && caption[0].checked) {
            imgclass += " captioned";
        };
        var classnames = document.getElementById('kupu-image-class');
        if (classnames && classnames.selectedIndex >= 0) {
            imgclass += " "+classnames.options[classnames.selectedIndex].value;
        } else {
            imgclass += ' image-inline';
        }
        imgclass = imgclass.strip();

        if (media !== undefined && this.tool['create_' + media]) {
            this.tool['create_' + media](uri, this.upload_title, imgclass, width, height);
        } else {
            this.tool.createImage(uri, this.upload_title, imgclass);
        }

        this.shared.newimages = 1;
        this.drawertool.closeDrawer();
    };


    this.save = function() {
        this.editor.resumeEditing();
        /* create an image in the iframe according to collected data
           from the drawer */
        var selxpath = '//resource[@selected]';
        var selnode = this.xmldata.selectSingleNode(selxpath);

        // If no image resource is selected, check for upload
        if (!selnode) {
            var uploadbutton = this.xmldata.selectSingleNode("/libraries/*[@selected]//uploadbutton");
            if (uploadbutton) {
                this.uploadImage();
            };
            return;
        };

        var sizeselector = document.getElementsByName('image-size-selector');
        if (sizeselector && sizeselector.length > 0) {
            sizeselector = sizeselector[0];
            var uri = sizeselector.options[sizeselector.selectedIndex].value;
        } else {
            var uri = selnode.selectSingleNode('uri/text()').nodeValue;
        }
        uri = uri.strip();  // needs kupuhelpers.js
        var alt = getFromSelector('image-alt');
        alt = alt?alt.value:undefined;

        var radios = document.getElementsByName('image-align');
        for (var i = 0; i < radios.length; i++) {
            if (radios[i].checked) {
                var imgclass = radios[i].value;
            };
        };

        var caption = document.getElementsByName('image-caption');
        if (caption && caption.length>0 && caption[0].checked) {
            imgclass += " captioned";
        };
        var classnames = document.getElementById('kupu-image-class');
        if (classnames && classnames.selectedIndex >= 0) {
            imgclass += " "+classnames.options[classnames.selectedIndex].value;
        }
        var media = document.getElementById('kupu-media').value;
        var width = document.getElementById('kupu-width').value;
        var height = document.getElementById('kupu-height').value;
        if (this.tool['create_' + media]) {
            this.tool['create_' + media](uri, alt, imgclass, width, height);
        } else {
            this.tool.createImage(uri, alt, imgclass);
        }
        kupu.content_changed = true;
        this.drawertool.closeDrawer();
    };
};

ImageLibraryDrawer.prototype = new LibraryDrawer;
ImageLibraryDrawer.prototype.shared = {}; // Shared data

function LinkLibraryDrawer(tool, xsluri, libsuri, searchuri, baseelement, selecturi) {
    /* a specific LibraryDrawer for links */

    this.drawertitle = "Insert Link";
    this.drawertype = "link";
    this.showanchors = "yes";

    if (tool) {
        this.init(tool, xsluri, libsuri, searchuri, baseelement, selecturi);
    }

    this.createContent = function() {
        var currnode = this.editor.getSelectedNode();
        var curranchor = this.editor.getNearestParentOfType(currnode, 'A');
        this.selectedSrc = curranchor?curranchor.href:null;
        this.options = {};
        if (curranchor) {
            this.options.link_name = curranchor.name || '';
            this.options.link_target = curranchor.target || '';
        }
        LinkLibraryDrawer.prototype.createContent.call(this);
    };

    this.save = function() {
        this.editor.resumeEditing();
        /* create a link in the iframe according to collected data
           from the drawer */
        var selxpath = '//resource[@selected]';
        var selnode = this.xmldata.selectSingleNode(selxpath);
        if (!selnode) {
            return;
        };

        var uri = selnode.selectSingleNode('uri/text()').nodeValue;
        uri = uri.strip() + this.getFragment();
        var title = '';
        title = selnode.selectSingleNode('title/text()').nodeValue;
        title = title.strip();

        // XXX requiring the user to know what link type to enter is a
        // little too much I think. (philiKON)
        var name = getFromSelector('link_name').value;
        var node = getFromSelector('link_target');
        var target = node && node.value;

        this.tool.createLink(uri, null, name, target, title, 'internal-link');
        this.drawertool.closeDrawer();
    };
};

LinkLibraryDrawer.prototype = new LibraryDrawer;
LinkLibraryDrawer.prototype.shared = {}; // Shared data

function AnchorDrawer(elementid, tool) {
    Drawer.call(this, elementid, tool);

    this.initialize = function(editor, tool) {
        Drawer.prototype.initialize.apply(this, [editor, tool]);
        this.panel = getBaseTagClass(this.element, 'div', 'kupu-panels');
        this.style1 = getFromSelector('kupu-bm-sel1');
        this.style2 = getFromSelector('kupu-bm-sel2');
        this.ostyle = getFromSelector('kupu-bm-outcls');
        this.nstyle = getFromSelector('kupu-bm-number');
        var tabs = getBaseTagClass(this.element, 'ul', 'kupu-tabs').getElementsByTagName('a');
        this.paralist = getBaseTagClass(this.element, 'div', 'kupu-bm-paras');
        this.checkall = getFromSelector('kupu-bm-checkall');

        for (var i = 0; i < tabs.length; i++) {
            addEventHandler(tabs[i], 'click', this.switchMode, this);
        }
        addEventHandler(this.checkall, 'click', this.checkAll, this);
        addEventHandler(this.style1, 'change', this.fillList, this);
        addEventHandler(this.style2, 'change', this.fillList, this);
        this.tool.fillStyleSelect(this.style1);
        this.tool.fillStyleSelect(this.style2);
        this.tool.fillStyleSelect(this.ostyle);
    };
    this.getMode = function() { /* tab 0, 1, or 2 */
        if (/kupu-ins-bm/.test(this.panel.className)) return 0;
        if (/kupu-anchor/.test(this.panel.className)) return 1;
        return 2;
    };

    this.checkAll = function() {
        var nodes = this.paralist.getElementsByTagName('input');
        var state = this.checkall.checked;
        for (var i = 0; i < nodes.length; i++) {
            var n = nodes[i];
            if (n.type=="checkbox" && !n.disabled) {
                nodes[i].checked = state;
            };
        };
    };

    this.fillList = function() {
        var el = newElement;
        while (this.paralist.firstChild) {
            this.paralist.removeChild(this.paralist.firstChild);
        }

        this.styleNames = ['', ''];

        var mode = this.getMode();
        var s = ['', ''];
        for (var idx=0; idx < (mode==2?2:1); idx++) {
            var sel = this['style'+(idx+1)];
            var i = sel.selectedIndex;
            if (i >= 0) {
                s[idx] = sel.options[i].value;
                this.styleNames[idx] = sel.options[i].firstChild.data;
            }
        }

        if (mode==1) {
            var inuse = this.tool.getAnchorsInUse();
        }
        var paras = (this.nodelist = this.tool.grubParas(s[0], s[1]));
        for (var i = 0; i < paras.length; i++) {
            var node = paras[i][0];
            var text = Sarissa.getText(node, true).strip().truncate(60);
            if (!text) continue;
            var content = document.createTextNode(text);
            var anchor = '';
            if (mode==1) {
                anchor = this.tool.getAnchor(node, true);
                if (anchor) {
                    anchor = '#'+anchor;
                }
            }
            var checked;
            switch (mode) {
                case 0: checked = i==0; break;
                case 1: checked = !!anchor; break;
                case 2: checked = this.checkall.checked; break;
            }
            var control = el('input', {
                'type': (mode==0)?"radio":"checkbox",
                checked: checked, title:'hello',
                name: "kupu-bm-paralist"});
            if (anchor && inuse && inuse[decodeURIComponent(anchor)]) {
                control.disabled = true;
            }

            var inner = [control, el('span', [content])];
            if (anchor) {
                inner.push(el('a', {href:anchor, className:'kupu-anchor-link',onclick:'return false;',
                    title:_('Right click to copy link')}, [anchor]));
            };
            var div = el('div', {className: "kupu-bm-level" + paras[i][1] },
                [el('label', inner)]);

            this.paralist.appendChild(div);
        };
    };
    this.createContent = function() {
        this.fillList();
        this.element.style.display = 'block';
        this.focusElement();
    };
    this.save = function() {
        var mode = this.getMode();
        var selected = this.paralist.getElementsByTagName('input');
        var ed = this.editor;

        ed.resumeEditing();

        if (mode==2) {
            var toc = ed.newElement('ul');
        };
        var lvl1=0, lvl2=0;
        for (var i = 0; i < selected.length; i++) {
            var nodeinfo = this.nodelist[i];
            var node = nodeinfo[0];
            var level = nodeinfo[1];
            if (selected[i].checked) {
                var a = this.tool.getAnchor(node);
                var caption = Sarissa.getText(node, true).strip().truncate(140);
                switch (mode) {
                case 0:
                    this.tool.createLink('#'+a, null, null, null, caption);
                    break;
                case 1:
                    break;
                case 2:
                    /* Insert TOC entry here */
                    var number;
                    if (level==0) {
                        number = ++lvl1;
                        lvl2 = 0;
                    } else {
                        number = lvl1 + '.' + (++lvl2);
                    };
                    var li = ed.newElement('li', {'className': 'level'+level},
                        [ed.newElement('a', {'href': '#'+a},
                        [ed.newText((this.nstyle.checked?number + ' ':'') + caption)])]);

                    if (level==0) {
                        toc.appendChild(li);
                    } else {
                        if (!toc.lastChild || toc.lastChild.nodeName.toLowerCase() != 'ul') {
                            toc.appendChild(ed.newElement('ul'));
                        }
                        toc.lastChild.appendChild(li);
                    };
                    break;
                };
            } else {
                if (mode==1) {
                    this.tool.removeAnchor(node);
                }
            };
        };
        if (mode==2 && toc.firstChild) {
            var o = this.ostyle.selectedIndex;
            if (o > 0) {
                var ostyle = this.ostyle.options[o].value.split('|');
                if (ostyle[0]=='ul') {
                    toc.className=ostyle[1];
                } else {
                    toc = ed.newElement(ostyle[0], {'className': ostyle[1]}, [toc]);
                };
            }
            var node = ed.getSelection().parentElement();
            if (node.nodeName.toLowerCase() == 'body') {
                node.insertBefore(toc, node.firstChild);
            } else {
                while (node.parentNode.nodeName.toLowerCase() != 'body') {
                    node = node.parentNode;
                }
                node.parentNode.insertBefore(toc, node);
            }
        }
        this.nodelist = null;
        this.drawertool.closeDrawer();
    };
    this.hide = function() {
        this.nodelist = null;
        Drawer.prototype.hide.apply(this, []);
    };
};

AnchorDrawer.prototype = new Drawer;

/* Function to suppress enter key in drawers */
function HandleDrawerEnter(event, clickid) {
    event = event || window.event;
    var key = event.which || event.keyCode;
    var button;
    if (key==13) {
        if (clickid) {
            button = document.getElementById(clickid);
            if (button && !button.disabled) {
                button.click();
            }
        }
        event.cancelBubble = true;
        if (event.stopPropagation) {
            event.stopPropagation();
        }
        event.returnValue = false;
        return false;
    }
    return true;
}




/* - kupuploneinit.js - */
/*****************************************************************************
 *
 * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/
/*extern DummyLogger noContextMenu */
// $Id: kupuploneinit.js 65886 2009-06-23 13:11:58Z ldr $

function initPloneKupu(editorId) {
    var prefix = '#'+editorId+' ';

    var iframe = getFromSelector(prefix+'iframe.kupu-editor-iframe');
    if (iframe._kupuIsInitialized) {
	    return window.kupu;
    };
    iframe._kupuIsInitialized = true;
    var textarea = getFromSelector(prefix+'textarea.kupu-editor-textarea');
    var form = textarea.form;
    var initialtext = textarea.value || (_SARISSA_IS_IE?'<p></p>':'<p><br></p>');

    // first we create a logger
    var l = new DummyLogger();

    // now some config values
    document.getElementById('kupu-config').innerHTML = unescape(document.getElementById('kupu-config-escaped').innerHTML);
    var conf = loadDictFromXML(document, prefix+'xml.kupuconfig');

    // the we create the document, hand it over the id of the iframe
    var doc = new KupuDocument(iframe);

    // now we can create the controller
    var kupu = (window.kupu = new KupuEditor(doc, conf, l));
    kupu.setHTMLBody(initialtext);

    // now we can create a UI object which we can use from the UI
    var ui = new KupuUI(prefix+'select.kupu-tb-styles');
    window.kupuui = ui;

    // the ui must be registered to the editor like a tool so it can be notified
    // of state changes
    kupu.registerTool('ui', ui); // XXX Should this be a different method?

    // function that returns a function to execute a button command
    var execCommand = function(cmd) {
        return function(button, editor) {
            editor.execCommand(cmd);
        };
    };

    var boldchecker = parentWithStyleChecker(['b', 'strong'],
                                             'font-weight', 'bold');
    var boldbutton = new KupuStateButton(prefix+'button.kupu-bold',
                                         execCommand('bold'),
                                         boldchecker,
                                         'kupu-bold',
                                         'kupu-bold-pressed');
    kupu.registerTool('boldbutton', boldbutton);

    var italicschecker = parentWithStyleChecker(['i', 'em'],
                                                'font-style', 'italic');
    var italicsbutton = new KupuStateButton(prefix+'button.kupu-italic',
                                            execCommand('italic'),
                                            italicschecker,
                                            'kupu-italic',
                                            'kupu-italic-pressed');
    kupu.registerTool('italicsbutton', italicsbutton);

    /* disabled
    var underlinechecker = parentWithStyleChecker(['u']);
    var underlinebutton = new KupuStateButton(prefix+'button.kupu-underline',
                                              execCommand('underline'),
                                              underlinechecker,
                                              'kupu-underline',
                                              'kupu-underline-pressed');
    kupu.registerTool('underlinebutton', underlinebutton);
    */

    var subscriptchecker = parentWithStyleChecker(['sub']);
    var subscriptbutton = new KupuStateButton(prefix+'button.kupu-subscript',
                                              execCommand('subscript'),
                                              subscriptchecker,
                                              'kupu-subscript',
                                              'kupu-subscript-pressed');
    kupu.registerTool('subscriptbutton', subscriptbutton);

    var superscriptchecker = parentWithStyleChecker(['super', 'sup']);
    var superscriptbutton = new KupuStateButton(prefix+'button.kupu-superscript',
                                                execCommand('superscript'),
                                                superscriptchecker,
                                                'kupu-superscript',
                                                'kupu-superscript-pressed');
    kupu.registerTool('superscriptbutton', superscriptbutton);

    var justifyleftbutton = new KupuButton(prefix+'button.kupu-justifyleft',
                                           execCommand('justifyleft'));
    kupu.registerTool('justifyleftbutton', justifyleftbutton);

    var justifycenterbutton = new KupuButton(prefix+'button.kupu-justifycenter',
                                             execCommand('justifycenter'));
    kupu.registerTool('justifycenterbutton', justifycenterbutton);

    var justifyrightbutton = new KupuButton(prefix+'button.kupu-justifyright',
                                            execCommand('justifyright'));
    kupu.registerTool('justifyrightbutton', justifyrightbutton);

    var outdentbutton = new KupuButton(prefix+'button.kupu-outdent', execCommand('outdent'));
    kupu.registerTool('outdentbutton', outdentbutton);

    var indentbutton = new KupuButton(prefix+'button.kupu-indent', execCommand('indent'));
    kupu.registerTool('indentbutton', indentbutton);

    var undobutton = new KupuButton(prefix+'button.kupu-undo', execCommand('undo'));
    kupu.registerTool('undobutton', undobutton);

    var redobutton = new KupuButton(prefix+'button.kupu-redo', execCommand('redo'));
    kupu.registerTool('redobutton', redobutton);

    var removeimagebutton = new KupuRemoveElementButton(prefix+'button.kupu-removeimage',
                                                        'img',
                                                        'kupu-removeimage');
    kupu.registerTool('removeimagebutton', removeimagebutton);

    var removelinkbutton = new KupuRemoveElementButton(prefix+'button.kupu-removelink',
                                                       'a',
                                                       'kupu-removelink');
    kupu.registerTool('removelinkbutton', removelinkbutton);

    // add some tools
    var colorchoosertool = new ColorchooserTool(prefix+'button.kupu-forecolor',
                                                prefix+'button.kupu-hilitecolor',
                                                prefix+'table.kupu-colorchooser');
    kupu.registerTool('colorchooser', colorchoosertool);

    var listtool = new ListTool(prefix+'button.kupu-insertunorderedlist',
                                prefix+'button.kupu-insertorderedlist',
                                prefix+'select.kupu-ulstyles',
                                prefix+'select.kupu-olstyles');
    kupu.registerTool('listtool', listtool);

    var definitionlisttool = new DefinitionListTool(prefix+'button.kupu-insertdefinitionlist');
    kupu.registerTool('definitionlisttool', definitionlisttool);

    var tabletool = new TableTool();
    kupu.registerTool('tabletool', tabletool);

    var anchortool = new AnchorTool();
    kupu.registerTool('anchortool', anchortool);

    var showpathtool = new ShowPathTool('kupu-showpath-field');
    kupu.registerTool('showpathtool', showpathtool);

    var sourceedittool = new SourceEditTool(prefix+'button.kupu-source',
                                            prefix+'textarea.kupu-editor-textarea');
    kupu.registerTool('sourceedittool', sourceedittool);

    var imagetool = noContextMenu(new ImageTool());
    kupu.registerTool('imagetool', imagetool);

    var linktool = noContextMenu(new LinkTool());
    kupu.registerTool('linktool', linktool);

    var zoom = new KupuZoomTool(prefix+'button.kupu-zoom',
                                prefix+'select.kupu-tb-styles',
                                prefix+'button.kupu-logo');
    kupu.registerTool('zoomtool', zoom);

    if (typeof KupuSpellChecker != 'undefined') {
        var spellchecker = new KupuSpellChecker('kupu-spellchecker-button',
                                                'kupu_library_tool/spellcheck');
        kupu.registerTool('spellchecker', spellchecker);
    } else {
        // hide the button when not available
        var sc = getFromSelector(prefix+'span.kupu-spellchecker-span');
        if (sc) sc.style.display = 'none';
    }

    // Use the generic beforeUnload handler if we have it:
    var beforeunloadTool = window.onbeforeunload && window.onbeforeunload.tool;
    if (beforeunloadTool) {
        var initialBody = kupu.getHTMLBody();
        beforeunloadTool.addHandler(function() {
            for (var n = textarea; n; n = n.parentNode) {
                if (n===document) {
                    return kupu.getHTMLBody() != initialBody;
                }
            }
            return false; /* textarea is no longer in the document */
        });
        beforeunloadTool.chkId[textarea.id] = function() { return false; };
        beforeunloadTool.addForm(form);
    }
    // Patch for bad AT format pulldown.
    var fmtname = textarea.name+'_text_format';
    var pulldown = form[fmtname];
    if (pulldown && pulldown.type=='select-one') {
        for (var i=0 ; i < pulldown.length; i++) {
            var opt = pulldown.options[i];
            opt.selected = opt.defaultSelected = (opt.value=='text/html');
        }
        pulldown.disabled = true;
        pulldown.removeAttribute('name');
        var hidden = document.createElement('input');
        hidden.type = 'hidden';
        hidden.name = fmtname;
        hidden.value = 'text/html';
        pulldown.parentNode.appendChild(hidden);
    };

    // Drawers...

    // Function that returns function to open a drawer
    var opendrawer = function(drawerid) {
        return function(button, editor) {
            drawertool.openDrawer(prefix+drawerid);
        };
    };

    var imagelibdrawerbutton = new KupuButton(prefix+'button.kupu-image',
                                              opendrawer('imagelibdrawer'));
    kupu.registerTool('imagelibdrawerbutton', imagelibdrawerbutton);

    var linklibdrawerbutton = new KupuButton(prefix+'button.kupu-inthyperlink',
                                             opendrawer('linklibdrawer'));
    kupu.registerTool('linklibdrawerbutton', linklibdrawerbutton);

    var linkdrawerbutton = new KupuButton(prefix+'button.kupu-exthyperlink',
                                          opendrawer('linkdrawer'));
    kupu.registerTool('linkdrawerbutton', linkdrawerbutton);

    var anchorbutton = new KupuButton(prefix+'button.kupu-anchors',
                                      opendrawer('anchordrawer'));
    kupu.registerTool('anchorbutton', anchorbutton);

    var tabledrawerbutton = new KupuButton(prefix+'button.kupu-table',
                                           opendrawer('tabledrawer'));
    kupu.registerTool('tabledrawerbutton', tabledrawerbutton);

    // create some drawers, drawers are some sort of popups that appear when a
    // toolbar button is clicked
    var drawertool = window.drawertool || new DrawerTool();
    window.drawertool = drawertool;
    kupu.registerTool('drawertool', drawertool);

    var drawerparent = prefix+'div.kupu-librarydrawer-parent';
    var xsl_uri = conf.xsl_uri;
    var link_resource = conf.link_resource;
    var image_resource = conf.image_resource;
    var lib_prefix = conf.lib_prefix;
    var search_prefix = conf.search_prefix;
    var select_prefix = conf.select_prefix;
    var linklibdrawer = new LinkLibraryDrawer(linktool,
                                              xsl_uri,
                                              lib_prefix+link_resource,
                                              search_prefix+link_resource,
                                              drawerparent,
                                              select_prefix+link_resource);
    drawertool.registerDrawer(prefix+'linklibdrawer', linklibdrawer, kupu);

    var imagelibdrawer = new ImageLibraryDrawer(imagetool,
                                                xsl_uri,
                                                lib_prefix+image_resource,
                                                search_prefix+image_resource,
                                                drawerparent,
                                                select_prefix+image_resource);
    drawertool.registerDrawer(prefix+'imagelibdrawer', imagelibdrawer, kupu);

    var linkdrawer = new LinkDrawer(prefix+'div.kupu-linkdrawer', linktool);
    drawertool.registerDrawer(prefix+'linkdrawer', linkdrawer, kupu);

    var anchordrawer = new AnchorDrawer(prefix+'div.kupu-anchordrawer', anchortool);
    drawertool.registerDrawer(prefix+'anchordrawer', anchordrawer, kupu);

    var tabledrawer = new TableDrawer(prefix+'div.kupu-tabledrawer', tabletool);
    drawertool.registerDrawer(prefix+'tabledrawer', tabledrawer, kupu);

    // register form submit handler, remove the drawer's contents before submitting
    // the form since it seems to crash IE if we leave them alone
    function prepareForm(event) {
        kupu.saveDataToField(this.form, this);
        var drawer = window.document.getElementById('kupu-librarydrawer');
        if (drawer) {
            drawer.parentNode.removeChild(drawer);
        }
    };
    addEventHandler(textarea.form, 'submit', prepareForm, textarea);

    function tabHandler(event) {
        event = event||window.event;
        if (event.keyCode!=9) { return; }
        if (!(/kupu-fulleditor-zoomed/.test(document.body.className))) {
            var form = textarea.form;
            var els = form.elements;
            var target;
            if (event.shiftKey) { // shift-tab goes backwards.
                for (var i = 0; i < els.length; i++) {
                    var el = els[i];
                    if (!el.disabled && el.offsetWidth && el.offsetHeight) {
                        target = el;
                    }
                    if (els[i]===textarea) break;
                }
            } else { // tab forwards
                for (var i = 0; i < els.length; i++) {
                    if (els[i]===textarea) break;
                }
                for (;i < els.length; i++) {
                    var el = els[i];
                    if (!el.disabled && el.offsetWidth && el.offsetHeight) {
                        target = el;
                        break;
                    }
                }
            }
            if (target) {
                window.focus();
                target.focus();
            } else { return; };
        }
        if (event.preventDefault) { event.preventDefault(); event.stopPropagation();}
        event.returnValue = false;
        return false;
    }
    var inner = kupu.getInnerDocument();
    kupu._addEventHandler(inner.documentElement, "keydown", tabHandler);

    kupu.initialize();
    return kupu;
};

// modify LinkDrawer so links don't have a target
LinkDrawer.prototype.target = '';
LinkLibraryDrawer.prototype.target = '';
if (!window.console) {
    window.console = new function() {
        this.log = function() {};
    };
}

