// 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

/*****************************************************************************
 *
 * Sarissa XML library version 0.9.6
 * Copyright (c) 2003 Manos Batsis, 
 * mailto: mbatsis at users full stop sourceforge full stop net
 * This software is distributed under the Kupu License. See
 * LICENSE.txt for license text. See the Sarissa homepage at
 * http://sarissa.sourceforge.net for more information.
 *
 *****************************************************************************

 * ====================================================================
 * About
 * ====================================================================
 * Sarissa cross browser XML library - IE XPath Emulation 
 * @version 0.9.6
 * @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
 *
 */
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 = new Array(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 */
    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(" "):new Array(sNsSet);
        oDoc._sarissa_xpathNamespaces = new Array(namespaces.length);
        for(var i=0;i < namespaces.length;i++){
            var ns = namespaces[i];
            var colonPos = ns.indexOf(":");
            var assignPos = ns.indexOf("=");
            if(colonPos == 5 && assignPos > colonPos+2){
                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 = new Array();
    /**
    * <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){
        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 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);
        return nodeList;
    };
    /**
    * <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 selectSingleNodes.</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;
        sExpr = "("+sExpr+")[1]";
        var nodeList = this.selectNodes(sExpr, ctx);
        if(nodeList.length > 0)
            return nodeList.item(0);
        else
            return null;
    };
    /**
    * <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.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 12353 2005-05-16 12:29:05Z duncan $

/*

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 addEventHandler(element, event, method, context) {
    /* method to add an event handler for both IE and Mozilla */
    var wrappedmethod = new ContextFixer(method, context);
    var args = new Array(null, null);
    for (var i=4; i < arguments.length; i++) {
        args.push(arguments[i]);
    };
    wrappedmethod.args = args;
    try {
        if (_SARISSA_IS_MOZ) {
            element.addEventListener(event, wrappedmethod.execute, false);
        } else if (_SARISSA_IS_IE) {
            element.attachEvent("on" + event, wrappedmethod.execute);
        } else {
            throw _("Unsupported browser!");
        };
        return wrappedmethod.execute;
    } catch(e) {
        alert(_('exception ${message} while registering an event handler ' +
                'for element ${element}, event ${event}, method ${method}',
                {'message': e.message, 'element': element,
                    'event': event,
                    'method': method}));
    };
};

function removeEventHandler(element, event, method) {
    /* method to remove an event handler for both IE and Mozilla */
    if (_SARISSA_IS_MOZ) {
        window.removeEventListener(event, method, false);
    } else if (_SARISSA_IS_IE) {
        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) {
    /* open and center a popup window */
    var sw = screen.width;
    var sh = screen.height;
    var left = sw / 2 - width / 2;
    var top = sh / 2 - height / 2;
    var win = window.open(url, 'someWindow', 
                'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top);
    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();
            if (dict[name] != undefined) {
                if (!dict[name].push) {
                    dict[name] = new Array(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) {
    this.document = document;
    this.selection = document.getWindow().getSelection();
    
    this.selectNodeContents = function(node) {
        /* select the contents of a node */
        this.selection.removeAllRanges();
        this.selection.selectAllChildren(node);
    };

    this.collapse = function(collapseToEnd) {
        try {
            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.document.getDocument().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);
                };
            }

            range.setEnd(afterNode, 0);
            range.setStart(afterNode, 0);
        }

        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 = this.document.getWindow().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);
                };
            };
            this.document.getWindow().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;
            };
            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.document.getDocument().createRange();
        arange.setStart(anode, aoffset);
        var orange = this.document.getDocument().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 != 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.document.getDocument().createRange();
        arange.setStart(anode, aoffset);
        var orange = this.document.getDocument().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 endoffset = 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() {
        /* return the selected node (or the node containing the selection) */
        // XXX this should be on a range object
        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 = range.commonAncestorContainer;

            // the following deals with cases where only a single child is
            // selected, e.g. after a click on an image
            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) {
                // XXX the additional conditions catch some invisible
                // intersections, but still not all of them
                if (range.intersectsNode(child) &&
                    !(child == startNode && startOffset == child.length) &&
                    !(child == endNode && endOffset == 0)) {
                    if (selectedChild) {
                        // current child is the second selected child found
                        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;
            var startparent = null;
            var startoffset = 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 = this.document.getWindow().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) {
        return this.selection.containsNode(node, true);
    }

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

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

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.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 = this.selection.createRange().duplicate();
        range.moveToElementText(node);
        range.moveStart('character', 1);
        range.moveStart('character', -1);
        range.moveEnd('character', -1);
        range.moveEnd('character', 1);
        range.select();
        this.selection = this.document.getDocument().selection;
    };

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

    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">');
            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() {
        /* 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.moveEnd = function(offset) {
        /* moves the end of the selection */
        var range = this.selection.createRange();
        range.moveEnd('character', offset);
        range.select();
    };

    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();
        } catch(e) {
        };
    }

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

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 = new Array();
        // 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 = new Array();
        for (var i=0; i < arguments.length - 3; i++) {
            args.push(arguments[i + 3]);
        }
        var id = this._createUniqueId();
        this.functions[id] = new Array(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 */
    var spacereg = /(\s+)/g;
    var copy = this;
    while (true) {
        var match = spacereg.exec(copy);
        if (!match) {
            return copy;
        };
        copy = copy.replace(match[0], ' ');
    };
};

String.prototype.entitize = function() {
    var ret = this.replace(/&/g, '&amp;');
    ret = ret.replace(/"/g, '&quot;');
    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(/&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();


// 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 18104 2005-10-03 14:10:11Z 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';
    
    // methods
    this.execCommand = function(command, arg) {
        /* delegate execCommand */
        if (arg === undefined) arg = null;
        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 = new Array(); // contentfilters
    
    this._designModeSetAttempts = 0;
    this._initialized = 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.logMessage(_('Editor initialized'));
    };

    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 = new Array(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=='mouseup' ||
                (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 != 'useCSS') {
                this.content_changed = true;
                // note the negation: the argument doesn't work as
                // expected...
                // Done here otherwise it doesn't always work or gets lost
                // after some commands
                this.getDocument().execCommand('useCSS', !this.config.use_css);
            };
        };
        this.getDocument().execCommand(command, param);
        var message = _('Command ${command} executed', {'command': command});
        if (param) {
            message = _('Command ${command} executed with parameter ${param}',
                            {'command': command, 'param': param});
        }
        this.updateState();
        this.logMessage(message);
    };

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

    this.getSelectedNode = function() {
        /* returns the selected node (read: parent) or none */
        return this.getSelection().parentElement();
    };

    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);
        */
    };

    this._initializeEventHandlers = function() {
        /* attache the event handlers to the iframe */
        // Initialize DOM2Event compatibility
        // XXX should come back and change to passing in an element
        this._addEventHandler(this.getInnerDocument(), "click", this.updateStateHandler, this);
        this._addEventHandler(this.getInnerDocument(), "dblclick", this.updateStateHandler, this);
        this._addEventHandler(this.getInnerDocument(), "keyup", this.updateStateHandler, this);
        this._addEventHandler(this.getInnerDocument(), "keyup", function() {this.content_changed = true}, this);
        this._addEventHandler(this.getInnerDocument(), "mouseup", this.updateStateHandler, this);
    };

    this._setDesignModeWhenReady = function() {
        /* Rather dirty polling loop to see if Mozilla is done doing it's
            initialization thing so design mode can be set.
        */
        this._designModeSetAttempts++;
        if (this._designModeSetAttempts > 25) {
            alert(_('Couldn\'t set design mode. Kupu will not work on this browser.'));
            return;
        };
        var success = false;
        try {
            this._setDesignMode();
            success = true;
        } catch (e) {
            // register a function to the timer_instance because 
            // window.setTimeout can't refer to 'this'...
            timer_instance.registerFunction(this, this._setDesignModeWhenReady, 100);
        };
        if (success) {
            // provide an 'afterInit' method on KupuEditor.prototype
            // for additional bootstrapping (after editor init)
            if (this.afterInit) {
                this.afterInit();
            };
        };
    };

    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 currange = this.getInnerDocument().selection.createRange();
            this._previous_range = currange;
        };
    };

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

    this._isDocumentSelected = function() {
        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 += Sarissa.serialize(bodies[i]);
        }
        return 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.escapeEntities(data);
    };

    // If we have multiple bodies this needs to remove the extras.
    this.setHTMLBody = function(text) {
        var bodies = this.getInnerDocument().documentElement.getElementsByTagName('body');
        for (var i = 0; i < bodies.length-1; i++) {
            bodies[i].parentNode.removeChild(bodies[i]);
        }
        bodies[bodies.length-1].innerHTML = text;
    };

    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 = new Array('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">' +
                            Sarissa.serialize(transform.getElementsByTagName("head")[0]) +
                            Sarissa.serialize(transform.getElementsByTagName("body")[0]) +
                            '</html>';
        } else {
            var contents = '<html>' + 
                            Sarissa.serialize(transform.getElementsByTagName("head")[0]) +
                            Sarissa.serialize(transform.getElementsByTagName("body")[0]) +
                            '</html>';
        };

        contents = this.escapeEntities(contents);

        if (this.config.compatible_singletons) {
            contents = this._fixupSingletons(contents);
        };
        
        return contents;
    };
    this.escapeEntities = function(xml) {
        // XXX: temporarily disabled
        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, '');
    }

    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.getInnerDocument().designMode = "Off";
            var iframe = this.getDocument().getEditable();
            iframe.style.position = iframe.style.position?"":"relative"; // Changing this disables designMode!
        }
        this.suspended = true;
    }
    
    this.resumeEditing = function() {
        if (!this.suspended) {
            return;
        }
        this.suspended = false;
        this.clearClass('kupu-modal');
        for (var id in this.tools) {
            this.tools[id].enable();
        }
        if (this.getBrowserName() == "IE") {
            this._restoreSelection();
            var body = this.getInnerDocument().getElementsByTagName('body')[0];
            body.setAttribute('contentEditable', 'true');
        } else {
            var doc = this.getInnerDocument();
            doc.designMode = "On";
            this.getSelection().restoreRange(this._previous_range);
        }
    }
}

// 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 21220 2005-12-16 15:47:11Z 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 = {};

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

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

    this.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);
    };
    
    this.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 (id in this.toolboxes) {
            this.toolboxes[id].updateState(selNode, event);
        };
    };

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

    this.disable = function() {
        // Called when the tool is disabled (e.g. for a modal form)
    }
    // private methods
    addEventHandler = addEventHandler;
    
    this._selectSelectItem = function(select, item) {
        this.editor.logMessage(_('Deprecation warning: KupuTool._selectSelectItem'));
    };
    this._fixTabIndex = function(element) {
        var tabIndex = this.editor.getDocument().getEditable().tabIndex-1;
        if (tabIndex && !element.tabIndex) {
            element.tabIndex = tabIndex;
        }
    }
}

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

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

    this.updateState = function(selNode, event) {
        /* update the toolbox according to the current iframe's situation */
    };
    
    this._selectSelectItem = function(select, item) {
        this.editor.logMessage(_('Deprecation warning: KupuToolBox._selectSelectItem'));
    };
};

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;
    button.disabled = "disabled";
    button.className += ' disabled';
}
function KupuButtonEnable(button) {
    button = button || this.button;
    button.disabled = "";
    button.className = button.className.replace(/ *\bdisabled\b/g, '');
}


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

function KupuButton(buttonid, commandfunc, tool) {
    /* Base prototype for kupu button tools */
    this.buttonid = buttonid;
    this.button = getFromSelector(buttonid);
    this.commandfunc = commandfunc;
    this.tool = tool;

    this.initialize = function(editor) {
        this.editor = editor;
        this._fixTabIndex(this.button);
        addEventHandler(this.button, 'click', this.execCommand, this);
    };

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

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

KupuButton.prototype = new KupuTool;
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
        */
        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]);
    this.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();
    };
}
KupuLateFocusStateButton.prototype = new KupuStateButton;

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.onclass = 'invisible';
    this.offclass = cssclass;
    this.pressed = false;

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

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

KupuRemoveElementButton.prototype = new KupuStateButton;

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);
    var paraoptions = [];
    var tableoptions = [];
    this.optionstate = -1;
    this.otherstyle = null;
    this.tablestyles = {};
    this.styles = {}; // use an object here so we can use the 'in' operator later on

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

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

    this.setTextStyleHandler = function(event) {
        this.setTextStyle(this.tsselect.options[this.tsselect.selectedIndex].value);
    };
    
    // event handlers
    this.basicButtonHandler = function(action) {
        /* event handler for basic actions (toolbar buttons) */
        this.editor.execCommand(action);
        this.editor.updateState();
    };

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

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

    this.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();
    };

    this.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();
    };

    this.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();
    };

    this.cleanStyles = function() {
        var options = this.tsselect.options;
        var parastyles = this.styles;
        var tablestyles = this.tablestyles;

        tableoptions.push([options[0].text, 'td|']);
        tablestyles['td'] = 0;
        paraoptions.push([options[0].text, 'p|']);
        parastyles['p'] = 0;
        while (options.length > 1) {
            opt = options[1];
            var v = opt.value;
            if (/^thead|tbody|table|t[rdh]\b/i.test(v)) {
                var otable = tableoptions;
                var styles = tablestyles;
            } else {
                var otable = paraoptions;
                var styles = parastyles;
            }
            if (v.indexOf('|') > -1) {
                var split = v.split('|');
                v = split[0].toLowerCase() + "|" + split[1];
            } else {
                v = v.toLowerCase()+"|";
            };
            otable.push([opt.text, v]);
            styles[v] = otable.length - 1;
            options[1] = null;
        }
        options[0] = null;
    }

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

        var valid = inTable ? tableoptions : paraoptions;

        while (options.length) options[0] = null;
        this.otherstyle = null;

        for (var i = 0; i < valid.length; i++) {
            var opt = document.createElement('option');
            opt.text = valid[i][0];
            opt.value = valid[i][1];
            options.add(opt);
        }
        select.selectedIndex = 0;
        this.optionstate = inTable;
    }
    
    this.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;
    }

    this.nodeStyle = function(node) {
        var currnode = node;
        var index = -1;
        var options = this.tsselect.options;
        this.styletag = undefined;
        this.classname = '';
        this.intable = false;

        while (currnode) {
            var tag = currnode.nodeName.toLowerCase();

            if (/^body$/.test(tag)) {
                if (!this.styletag) {
                    // Force style setting
                    //this.setTextStyle(options[0].value, true);
                    // Forced style messes up in Firefox: return -1 to
                    // indicate no style 
                    return -1;
                }
                break;
            }
            if (/^(p|div|h.|ul|ol|dl|menu|dir|pre|blockquote|address|center)$/.test(tag)) {
                index = this.setIndex(currnode, tag, index, this.styles);
            }
            if (/^thead|tbody|table|t[rdh]$/.test(tag)) {
                this.intable = true;
                index = this.setIndex(currnode, tag, index, this.tablestyles);

                if (index > 0 || tag=='table') {
                    return index; // Stop processing if in a table
                }
            }
            currnode = currnode.parentNode;
        }
        return index;
    }

    this.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
        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 = document.createElement('option');
            opt.text = caption;
            this.otherstyle = opt;
            this.tsselect.options.add(opt);

            index = this.tsselect.length-1;
        }
        this.tsselect.selectedIndex = Math.max(index,0);
    };

    this._cleanNode = function(node) {
                /* 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, remove the node itself.
                 */
        var len = node.childNodes.length;
        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) {
            node.parentNode.removeChild(node);
        };
    }

    this._cleanCell = function(eltype, classname) {
        var selNode = this.editor.getSelectedNode();
        var el = this.editor.getNearestParentOfType(selNode, eltype);
        if (!el) {
                // Maybe changing type
            el = this.editor.getNearestParentOfType(selNode, eltype=='TD'?'TH':'TD');
        }
        if (!el) return;

            // Remove formatted div or p from a cell
        var node, nxt, n;
        for (node = el.firstChild; node;) {
            if (/DIV|P/.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;
            }
        }
        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
        if (classname) {
            el.className = classname;
        } else {
            el.removeAttribute("class");
            el.removeAttribute("className");
        }

    }

    this._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(el);
            this._cleanNode(parent);
        } else {
            this._cleanNode(el);
        }
        // now set the classname
        if (classname) {
            el.className = classname;
        } else {
            el.removeAttribute("class");
            el.removeAttribute("className");
        }
    }
    this.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 command = eltype;
            // first create the element, then find it and set the classname
        if (this.editor.getBrowserName() == 'IE') {
            command = '<' + eltype + '>';
        };
        if (/T[RDH]/.test(eltype)) {
            this._cleanCell(eltype, classname);
        }
        else {
            this.editor.getDocument().execCommand('formatblock', command);

                // now get a reference to the element just added
            var selNode = this.editor.getSelectedNode();
            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();
        }
    };
  
    this.createContextMenuElements = function(selNode, event) {
        var ret = new Array();
        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;
    };
    this.disable = function() {
        this.tsselect.disabled = "disabled";
    }
    this.enable = function() {
        this.tsselect.disabled = "";
    }
}

KupuUI.prototype = new KupuTool;

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

    this.initialize = function(editor) {
        /* attach the event handlers */
        this.editor = editor;
        
        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();

        this.editor.logMessage(_('Colorchooser tool initialized'));
    };

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

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

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

    this.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');
        this.editor.execCommand(this.command, cell.getAttribute('bgColor'));
        this.hide();
    
        this.editor.logMessage(_('Color chosen'));
    };

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

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

    this.createColorchooser = function(table) {
        /* create the colorchooser table */
        
        var chunks = new Array('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;
    };
    this.enable = function() {
        KupuButtonEnable(this.fgcolorbutton);
        KupuButtonEnable(this.hlcolorbutton);
    }
    this.disable = function() {
        KupuButtonDisable(this.fgcolorbutton);
        KupuButtonDisable(this.hlcolorbutton);
    }
}

ColorchooserTool.prototype = new KupuTool;

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

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

    this.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;
                    }
                }
            }
        }

        this.editor.logMessage(_('Property tool initialized'));
    };

    this.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'));
    };
}

PropertyTool.prototype = new KupuTool;

function LinkTool() {
    /* Add and update hyperlinks */
    
    this.initialize = function(editor) {
        this.editor = editor;
        this.editor.logMessage(_('Link tool initialized'));
    };
    
    this.createLinkHandler = function(event) {
        /* create a link according to a url entered in a popup */
        var linkWindow = openPopup('kupupopups/link.html', 300, 200);
        linkWindow.linktool = this;
        linkWindow.focus();
    };

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

    this.formatSelectedLink = function(url, type, name, target, title) {
        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);
            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);
                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
    this.createLink = function(url, type, name, target, title) {
        if (!this.formatSelectedLink(url, type, name, target, title)) {
            // No links inside or outside.
            this.editor.execCommand("CreateLink", url);
            if (!this.formatSelectedLink(url, type, name, target, title)) {
                // Insert link with no text selected, insert the title
                // or URI instead.
                var doc = this.editor.getInnerDocument();
                linkel = doc.createElement("a");
                linkel.setAttribute('href', url);
                linkel.setAttribute('class', 'generated');
                this.editor.getSelection().replaceWithNode(linkel, true);
                this.updateLink(linkel, url, type, name, target, title);
            };
        }
        this.editor.logMessage(_('Link added'));
        this.editor.updateState();
    };
    
    this.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);
        
        this.editor.logMessage(_('Link removed'));
        this.editor.updateState();
    };
    
    this.createContextMenuElements = function(selNode, event) {
        /* create the 'Create link' or 'Remove link' menu elements */
        var ret = new Array();
        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;
    };
}

LinkTool.prototype = new KupuTool;

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;
    
    this.initialize = function(tool, editor) {
        /* attach the event handlers */
        this.tool = tool;
        this.editor = editor;
        addEventHandler(this.input, "blur", this.updateLink, this);
        addEventHandler(this.button, "click", this.addLink, this);
    };

    this.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 = '';
        }
    };
    
    this.addLink = function(event) {
        /* add a link */
        var url = this.input.value;
        this.tool.createLink(url);
    };
    
    this.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.logMessage(_('Link modified'));
    };
};

LinkToolBox.prototype = new LinkToolBox;

function ImageTool() {
    /* Image tool to add images */
    
    this.initialize = function(editor) {
        /* attach the event handlers */
        this.editor = editor;
        this.editor.logMessage(_('Image tool initialized'));
    };

    this.createImageHandler = function(event) {
        /* create an image according to a url entered in a popup */
        var imageWindow = openPopup('kupupopups/image.html', 300, 200);
        imageWindow.imagetool = this;
        imageWindow.focus();
    };

    this.createImage = function(url, alttext, imgclass) {
        /* create an image */
        var img = this.editor.getInnerDocument().createElement('img');
        img.src = url;
        img.removeAttribute('height');
        img.removeAttribute('width');
        if (alttext) {
            img.alt = alttext;
        };
        if (imgclass) {
            img.className = imgclass;
        };
        img = this.editor.insertNodeAtSelection(img, 1);
        this.editor.logMessage(_('Image inserted'));
        this.editor.updateState();
        return img;
    };

    this.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;
        };
    };

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

ImageTool.prototype = new KupuTool;

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;

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

    this.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;
            };
        };
    };

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

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

ImageToolBox.prototype = new KupuToolBox;

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

    // XXX There are some awfully long methods in here!!
    this.createContextMenuElements = function(selNode, event) {
        var table =  this.editor.getNearestParentOfType(selNode, 'table');
        if (!table) {
            ret = new Array();
            var el = new ContextMenuElement(_('Add table'), this.addPlainTable, this);
            ret.push(el);
            return ret;
        } else {
            var ret = new Array();
            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;
        };
    };

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

    this.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();

        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 (i=0; i < cols; i++) {
                var th = doc.createElement("th");
                th.appendChild(doc.createTextNode("Col " + i+1));
                tr.appendChild(th);
            }
            thead.appendChild(tr);
            table.appendChild(thead);
        }

        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);

        this.editor.logMessage(_('Table added'));
        this.editor.updateState();
        return table;
    };

    this._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);
            };
        };
    };
    
    this._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);
        };
    };

    this.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);
        }
        
        this.editor.focusDocument();
        this.editor.logMessage(_('Table row added'));
    };

    this.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);

        this.editor.focusDocument();
        this.editor.logMessage(_('Table row removed'));
    };

    this.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 currtr = this.editor.getNearestParentOfType(currnode, 'TR');
        var currtable = this.editor.getNearestParentOfType(currnode, 'TABLE');
        
        // get the current index
        var tdindex = this._getColIndex(currtd);
        // XXX this looks like a debug message, remove
        this.editor.logMessage(_('tdindex: ${tdindex}'));

        // 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++;
                    }
                }
            }
        }
        this.editor.focusDocument();
        this.editor.logMessage(_('Table column added'));
    };

    this.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];
            var relevant_rowspan = 0;
            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;
                    }
                    var colspan = cell.colSpan;
                    if (currindex == currcolindex) {
                        tr.removeChild(cell);
                        break;
                    }
                    currindex++;
                }
            }
        }
        this.editor.focusDocument();
        this.editor.logMessage(_('Table column deleted'));
    };

    this.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);
        this.editor.logMessage(_('Table removed'));
    };

    this.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 || 
                    (currtbody.nodeName.toUpperCase() != "THEAD" &&
                        currtbody.nodeName.toUpperCase() != "TBODY")) {
                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++;
                }
            }
        }
    };

    this.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;
        }
    };

    this._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;
    };

    this._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;
    };

    this.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);
    };

    this._isBodyRow = function(row) {
        for (var node = row.firstChild; node; node=node.nextSibling) {
            if (/TD/.test(node.nodeName)) {
                return true;
            }
        }
        return false;
    }

    this._cleanCell = function(el) {
        dump('_cleanCell('+el.innerHTML+')\n');
        // Remove formatted div or p from a cell
        var node, nxt, n;
        for (node = el.firstChild; node;) {
            if (/DIV|P/.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');
    }
    this._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;
    }

    this._cleanRows = function(rows, container, numcols) {
        // now walk through all rows to clean them up
        for (var i=0; i < rows.length; i++) {
            dump("row "+i+'\n');
            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;) {
                dump("child\n");
                var nxt = node.nextSibling;
                if (/TD|TH/.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 (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);
            };
        };
    };

    this._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;
            if (/TR/.test(node.nodeName)) {
                brows.push(node);
            } else if (/THEAD|TBODY|TFOOT/.test(node.nodeName)) {
                var rows = nodeName=='THEAD' ? hrows : nodeName=='TFOOT' ? frows : brows;
                for (var inode = node.firstChild; inode; inode = inode.nextSibling) {
                    if (/TR/.test(inode.nodeName)) {
                        rows.push(inode);
                    };
                };
            };
        };
        /* Extract thead and tfoot from tbody */
        dump('extract head and foot\n');
        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);
        }
        dump('count cols\n');
        // 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);

        dump('clean rows\n');
        // 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
        dump('remove old\n');
        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);
        dump('finish up\n');

        this.editor.focusDocument();
        this.editor.logMessage(_('Table cleaned up'));
    };

    this.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]);
        };
    };
};

TableTool.prototype = new KupuTool;

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

    // XXX There are some awfully long methods in here!!
    

    // 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.fixallbutton = getFromSelector(fixallbuttonid);
    this.toolboxel = getFromSelector(toolboxid);
    this.plainclass = plainclass;
    this.activeclass = activeclass;

    // register event handlers
    this.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.tool.addTableRow, this.tool);
        addEventHandler(this.delrowbutton, "click", this.tool.delTableRow, this.tool);
        addEventHandler(this.addcolbutton, "click", this.tool.addTableColumn, this.tool);
        addEventHandler(this.delcolbutton, "click", this.tool.delTableColumn, this.tool);
        addEventHandler(this.alignselect, "change", this.setColumnAlign, this);
        addEventHandler(this.classselect, "change", this.setTableClass, this);
        addEventHandler(this.fixbutton, "click", this.tool.fixTable, this.tool);
        addEventHandler(this.fixallbutton, "click", this.tool.fixAllTables, this.tool);
        this.addtablediv.style.display = "block";
        this.edittablediv.style.display = "none";
        this.editor.logMessage(_('Table tool initialized'));
    };

    this.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;
            };
        };
    };

    this.addTable = function() {
        /* add a table */
        var rows = this.newrowsinput.value;
        var cols = this.newcolsinput.value;
        var makeHeader = this.makeheaderinput.checked;
        // XXX getFromSelector
        var classchooser = getFromSelector("kupu-table-classchooser-add");
        var tableclass = this.classselect.options[this.classselect.selectedIndex].value;
        
        this.tool.createTable(rows, cols, makeHeader, tableclass);
    };

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

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

TableToolBox.prototype = new KupuToolBox;

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'
                            };
    
    this.initialize = function(editor) {
        /* attach event handlers */
        this.editor = editor;
        this._fixTabIndex(this.addulbutton);
        this._fixTabIndex(this.addolbutton);
        this._fixTabIndex(this.ulselect);
        this._fixTabIndex(this.olselect);

        addEventHandler(this.addulbutton, "click", this.addUnorderedList, this);
        addEventHandler(this.addolbutton, "click", this.addOrderedList, this);
        addEventHandler(this.ulselect, "change", this.setUnorderedListStyle, this);
        addEventHandler(this.olselect, "change", this.setOrderedListStyle, this);
        this.ulselect.style.display = "none";
        this.olselect.style.display = "none";

        this.editor.logMessage(_('List style tool initialized'));
    };

    this._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')];
        }
        selectSelectItem(onselect, currstyle);
        offselect.style.display = "none";
        onselect.style.display = "inline";
        offselect.selectedIndex = 0;
    };

    this.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;
            }
        }
        with(this.ulselect) {
            selectedIndex = 0;
            style.display = "none";
        };
        with(this.olselect) {
            selectedIndex = 0;
            style.display = "none";
        };
    };

    this.addList = function(command) {
        this.ulselect.style.display = "inline";
        this.olselect.style.display = "none";
        this.editor.execCommand(command);
        this.editor.focusDocument();
    };
    this.addUnorderedList = function() {
        /* add an unordered list */
        this.addList("insertunorderedlist");
    };

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

    this.setListStyle = function(tag, select) {
        /* set the type of an ul */
        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();
        this.editor.logMessage(_('List style changed'));
    };

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

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

    this.enable = function() {
        KupuButtonEnable(this.addulbutton);
        KupuButtonEnable(this.addolbutton);
        this.ulselect.disabled = "";
        this.olselect.disabled = "";
    }
    this.disable = function() {
        KupuButtonDisable(this.addulbutton);
        KupuButtonDisable(this.addolbutton);
        this.ulselect.disabled = "disabled";
        this.olselect.disabled = "disabled";
    }
};

ListTool.prototype = new KupuTool;

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

    this.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;
        while (currnode != null && currnode.nodeName != '#document') {
            if (currnode.nodeName.toLowerCase() == 'a') {
                url = currnode.getAttribute('href');
            };
            path = '/' + currnode.nodeName.toLowerCase() + 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);
        };
    };
};

ShowPathTool.prototype = new KupuTool;

function ViewSourceTool() {
    /* tool to provide a 'show source' context menu option */
    this.sourceWindow = null;
    
    this.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);
    };
    
    this.createContextMenuElements = function(selNode, event) {
        /* create the context menu element */
        return new Array(new ContextMenuElement(_('View source'), this.viewSource, this));
    };
};

ViewSourceTool.prototype = new KupuTool;

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);
    
    this.initialize = function(editor) {
        /* initialize the tool */
        this.editor = editor;
        this._fixTabIndex(this.dlbutton);
        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
    this.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);
                };
            };
        };
    };

    this.handleTabPress = function(selNode) {
    };

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

    this._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);
        };
    };

    this.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);
    };

    this.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;
    };

    this.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();
    };

    this.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();
    };

    this._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;
        };
    };
};

DefinitionListTool.prototype = new KupuTool;

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;

        this.baseinitialize(editor);
        this.button.tabIndex = this.editor.document.editable.tabIndex;
        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());

        this.editor.logMessage(_('Zoom tool initialized'));
    };
};

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;
    var body = document.body;

    if (window.innerWidth) {
        var width = window.innerWidth;
        var height = window.innerHeight;
    } else if (document.documentElement) {
        var width = document.documentElement.offsetWidth-5;
        var height = document.documentElement.offsetHeight-5;
    } else {
        var width = document.body.offsetWidth-5;
        var height = document.body.offsetHeight-5;
    }
    width = width + 'px';
    var offset = iframe.offsetTop;
    if (sourceArea) offset = sourceArea.offsetTop-1;
    // XXX: TODO: Using wrong values here, figure out why.
    var nheight = Math.max(height - offset -1/*top border*/, 10);
    nheight = nheight + '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 = height;
    }
}

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];
    if (zoom) {
        html.style.overflow = 'hidden';
        window.scrollTo(0, 0);
        editor.setClass(zoomClass);
        body.className += ' '+zoomClass;
        this.onresize();
    } else {
        html.style.overflow = '';
        var fulleditor = iframe.parentNode;
        fulleditor.style.width = '';
        body.className = 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 = '';
        };
    }
    var doc = editor.getInnerDocument();
    // Mozilla needs this. Yes, really!
    doc.designMode=doc.designMode;

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


//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 9879 2005-03-18 12:04:00Z yuppie $


//----------------------------------------------------------------------------
// 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 18097 2005-10-03 09:51:28Z 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,
                            '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,
                            'h7': 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,
                            'span': 1,
                            'strong': 1,
                            'style': 1,
                            'sub': 1,
                            'sup': 1,
                            'table': 1,
                            'tbody': 1,
                            'td': 1,
                            'tfoot': 1,
                            'th': 1,
                            'thead': 1,
                            'tr': 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
    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];
            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]];
        }
    }
    if (editor.getBrowserName()=="IE") {
        this._getTagName = function(htmlnode) {
            var nodename = htmlnode.nodeName.toLowerCase();
            if (htmlnode.scopeName && htmlnode.scopeName != "HTML") {
                nodename = htmlnode.scopeName+':'+nodename;
            }
            return nodename;
        }
    } else {
        this._getTagName = function(htmlnode) {
            return htmlnode.nodeName.toLowerCase();
        }
    };

    // 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'];
        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.inline_forms = ['input','select','textarea','label','button'];
        this.misc_inline = ['ins','del'];
        this.misc = ['noscript'].concat(this.misc_inline);
        this.inline = ['a'].concat(this.special, this.fontstyle, this.phrase, this.inline_forms);

        this.Inline = ['#PCDATA'].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 = ['#PCDATA','form'].concat(this.block, this.inline);
    }(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.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({});

    // 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'], ['#PCDATA']);
        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|applet|big|small|sub|sup|font|basefont"));
        setStates(['a'], validation._exclude(el.Inline, "a"));
        setStates(['applet', 'object'], ['#PCDATA', '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'], ['#PCDATA','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']);

    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);
    }
    // Set up filters for attributes.
    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);
        }
        // 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 nodeName = filter._getTagName(htmlnode);
            var bad = filter.badTagAttributes[nodeName];
            for (var i=0; i < htmlnode.attributes.length; i++) {
                var attr = htmlnode.attributes[i];
                if (bad && bad.contains(attr.name)) {
                    continue;
                };
                if (attr.value !== null && attr.value !== undefined) {
                    xhtmlnode.setAttribute(attr.name, attr.value);
                };
            };
        }
        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);
            }
            this['http-equiv'] = function(name, htmlnode, xhtmlnode) {
                var val = htmlnode.httpEquiv;
                if (val) xhtmlnode.setAttribute('http-equiv', val);
            }
            this['xml:lang'] = this['xml:space'] = function(name, htmlnode, xhtmlnode) {
                try {
                    var val = htmlnode.getAttribute(name);
                    if (val) xhtmlnode.setAttribute(name, val);
                } catch(e) {
                }
            }
        }
        this.rowspan = this.colspan = function(name, htmlnode, xhtmlnode) {
            var val = htmlnode.getAttribute(name);
            if (val && val != '1') xhtmlnode.setAttribute(name, val);
        }
        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);
        }
    }(this, editor);

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

    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 = exclude.c;
            if (!c.length) c = [c];
            for (var i = 0; i < c.length; i++) {
                this.excludeTagAttributes(c[i].t, c[i].a);
            }
        }
        if (exclude.xstyle) {
            var s = exclude.xstyle;
            for (var i = 0; i < s.length; i++) {
                this.styleWhitelist[s[i]] = 1;
            }
        }
        if (exclude['class']) {
            var c = exclude['class'];
            for (var i = 0; i < c.length; i++) {
                this.classBlacklist[c[i]] = 1;
            }
        }
    };

    // Copy all valid attributes from htmlnode to xhtmlnode.
    this._copyAttributes = function(htmlnode, xhtmlnode, valid) {
        if (valid.contains('*')) {
            // allow all attributes on this tag
            this.attrFilters['*'](name, htmlnode, xhtmlnode);
            return;
        };
        for (var i = 0; i < valid.length; i++) {
            var name = valid[i];
            var filter = this.attrFilters[name];
            if (filter) filter(name, htmlnode, xhtmlnode);
        }
    }

    this._convertToSarissaNode = function(ownerdoc, htmlnode, xhtmlparent) {
        return this._convertNodes(ownerdoc, htmlnode, xhtmlparent, new this.Set(['html']));
    };
    
    this._convertNodes = function(ownerdoc, htmlnode, xhtmlparent, permitted) {
        var name, 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)
                this._copyAttributes(htmlnode, xhtmlnode, validattrs);
        }

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

        if (kids.length == 0) {
            if (htmlnode.text && htmlnode.text != "" &&
                (nostructure || permittedChildren['#PCDATA'])) {
                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 == '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['#PCDATA'])
                        parentnode.appendChild(ownerdoc.createTextNode(kid.nodeValue));
                } else if (kid.nodeType == 4) {
                    if (nostructure || permittedChildren['#PCDATA'])
                        parentnode.appendChild(ownerdoc.createCDATASection(kid.nodeValue));
                }
            }
        } 
        return xhtmlnode;
    };
}



//kupucontextmenu.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: kupucontextmenu.js 9879 2005-03-18 12:04:00Z yuppie $


//----------------------------------------------------------------------------
// ContextMenu
//----------------------------------------------------------------------------

function ContextMenu() {
    /* the contextmenu */
    this.contextmenu = null;
    this.seperator = 1;

    this.initialize = function(editor) {
        /* set the event handlers and such */
        this.editor = editor;
        // needs some work since it won't work for more than one editor
        addEventHandler(this.editor.getInnerDocument(), "contextmenu", this.createContextMenu, this);
        //addEventHandler(editor.getInnerDocument(), "focus", this.hideContextMenu, this);
        addEventHandler(document, "focus", this.hideContextMenu, this);
        addEventHandler(editor.getInnerDocument(), "mousedown", this.hideContextMenu, this);
        addEventHandler(document, "mousedown", this.hideContextMenu, this);
    };

    this.createContextMenu = function(event) {
        /* Create and show the context menu 
        
            The method will ask all tools for any (optional) elements they
            want to add the menu and when done render it
        */
        if (event.stopPropagation) {
            event.stopPropagation();
        };
        event.returnValue = false;
        if (this.editor.getBrowserName() == 'IE') {
            this.editor._saveSelection();
        };
        // somehow Mozilla on Windows seems to generate the oncontextmenu event
        // several times on each rightclick, here's a workaround
        if (this.editor.getBrowserName() == 'Mozilla' && this.contextmenu) {
            return false;
        };
        this.hideContextMenu();
        var selNode = this.editor.getSelectedNode();
        var elements = new Array();
        for (var id in this.editor.tools) {
            var tool = this.editor.tools[id];
            // alas, some people seem to want backward compatibility ;)
            if (tool.createContextMenuElements) {
                var els = tool.createContextMenuElements(selNode, event);
                elements = elements.concat(els);
            };
        };
        // remove the last seperator
        this._createNewContextMenu(elements, event);
        this.last_event = event;
        return false;
    };

    this.hideContextMenu = function(event) {
        /* remove the context menu from view */
        if (this.contextmenu) {
            try {
                window.document.getElementsByTagName('body')[0].removeChild(this.contextmenu);
            } catch (e) {
                // after some commands, the contextmenu will be removed by 
                // the browser, ignore those cases
            };
            this.contextmenu = null;
        };
    };

    this._createNewContextMenu = function(elements, event) {
        /* add the elements to the contextmenu and show it */
        var doc = window.document;
        var menu = doc.createElement('div');
        menu.contentEditable = false;
        menu.designMode = 'Off';
        this._setMenuStyle(menu);
        for (var i=0; i < elements.length; i++) {
            var element = elements[i];
            if (element !== this.seperator) {
                var div = doc.createElement('div');
                div.style.width = '100%';
                var label = doc.createTextNode('\u00a0' + element.label);
                div.appendChild(label);
                menu.appendChild(div);
                // set a reference to the div on the element
                element.element = div;
                addEventHandler(div, "mousedown", element.action, element.context);
                addEventHandler(div, "mouseover", element.changeOverStyle, element);
                addEventHandler(div, "mouseout", element.changeNormalStyle, element);
                addEventHandler(div, "mouseup", this.hideContextMenu, this);
            } else {
                var hr = doc.createElement('hr');
                menu.appendChild(hr);
            };
        };
        // now move the menu to the right position
        var iframe = this.editor.getDocument().getEditable();
        var left = event.clientX;
        var top = event.clientY;
        var currnode = iframe;
        if (this.editor.getBrowserName() == 'IE') {
            while (currnode) {
                left += currnode.offsetLeft + currnode.clientLeft;
                top += currnode.offsetTop + currnode.clientTop;
                currnode = currnode.offsetParent;
            };
        } else {
            while (currnode) {
                left += currnode.offsetLeft;
                top += currnode.offsetTop;
                currnode = currnode.offsetParent;
            };
        };
        menu.style.left = left + 'px';
        menu.style.top = top + 'px';
        menu.style.visibility = 'visible';
        addEventHandler(menu, 'focus', function() {this.blur}, menu)
        doc.getElementsByTagName('body')[0].appendChild(menu);
        this.contextmenu = menu;
    };
    
    this._setMenuStyle = function(menu) {
        /* set the styles for the menu

            to change the menu style, override this method
        */
        menu.style.position = 'absolute';
        menu.style.backgroundColor = 'white';
        menu.style.fontFamily = 'Verdana, Arial, Helvetica, sans-serif';
        menu.style.fontSize = '12px';
        menu.style.lineHeight = '16px';
        menu.style.borderWidth = '1px';
        menu.style.borderStyle = 'solid';
        menu.style.borderColor = 'black';
        menu.style.cursor = 'default';
        menu.style.width = "8em";
    };

    this._showOriginalMenu = function(event) {
        window.document.dispatchEvent(this._last_event);
    };
};

function ContextMenuElement(label, action, context) {
    /* context menu element struct
    
        should be returned (optionally in a list) by the tools' 
        createContextMenuElements methods
    */
    this.label = label; // the text shown in the menu
    this.action = action; // a reference to the method that should be called
    this.context = context; // a reference to the object on which the method
                            //  is defined
    this.element = null; // the contextmenu machinery will add a reference 
                            // to the element here

    this.changeOverStyle = function(event) {
        /* set the background of the element to 'mouseover' style

            override only for the prototype, not for individual elements
            so every element looks the same
        */
        this.element.style.backgroundColor = 'blue';
    };

    this.changeNormalStyle = function(event) {
        /* set the background of the element back to 'normal' style

            override only for the prototype, not for individual elements
            so every element looks the same
        */
        this.element.style.backgroundColor = 'white';
    };
};


//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 9879 2005-03-18 12:04:00Z yuppie $
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 hrefparts = href.split('/');
    return 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+1 == urlparts.length && last=='emptypage') {
                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 = new Array();
                var i = 0;
                for (; i+common < hrefparts.length-1; i++) {
                    path[i] = '..';
                };
                while (common < urlparts.length) {
                    path[i++] = urlparts[common++];
                };
                if (i==0) {
                    path[i++] = '.';
                }
                str = path.join('/');
                if (anchor) {
                    str = [str,anchor].join('#');
                }
                str = tag + str+'"';
            };
            return str;
        });
};

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 = kupu.getXMLBody(transform);
    if (/^<body[^>]*>(<\/?(p|br)[^>]*>|\&nbsp;)*<\/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
    var document = form.ownerDocument;

    field.value = contents;
    
    kupu.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;

    this.getSourceArea = function() {
        return getFromSelector(sourceareaid);
    }

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

    this.initialize = function(editor) {
        /* attach the event handlers */
        this.editor = editor;
        this._fixTabIndex(this.sourceButton);
        addEventHandler(this.sourceButton, "click", this.switchSourceEdit, this);
        this.editor.logMessage(_('Source edit tool initialized'));
    };
 
    this.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, "");
                window.status = '';
            } else {
                data = kupu.getHTMLBody();
            }
            sourcearea.value = data;
            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.sourcemode = !this.sourcemode;
    };
    this.enable = function() {
        KupuButtonEnable(this.sourceButton);
    }
    this.disable = function() {
        KupuButtonDisable(this.sourceButton);
    }
};

SourceEditTool.prototype = new KupuTool;

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

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

    this._currently_editing = null;

};

MultiSourceEditTool.prototype = new SourceEditTool;


//kupudrawers.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: kupudrawers.js 15812 2005-08-09 12:30:34Z duncan $

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) {
        /* open a drawer */
        if (this.current_drawer) {
            this.closeDrawer();
        };
        var drawer = this.drawers[id];
        if (this.isIE) {
            drawer.editor._saveSelection();
        }
        drawer.createContent();
        drawer.editor.suspendEditing();
        this.current_drawer = drawer;
    };

    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;
    };

//     this.getDrawerEnv = function(iframe_win) {
//         var drawer = null;
//         for (var id in this.drawers) {
//             var ldrawer = this.drawers[id];
//             // Note that we require drawers to provide us with an
//             // element property!
//             if (ldrawer.element.contentWindow == iframe_win) {
//                 drawer = ldrawer;
//             };
//         };
//         if (!drawer) {
//             this.editor.logMessage("Drawer not found", 1);
//             return;
//         };
//         return {
//             'drawer': drawer,
//             'drawertool': this,
//             'tool': drawer.tool
//         };
//     };
};

DrawerTool.prototype = new KupuTool;

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

    this.element = getFromSelector(elementid);
    this.tool = tool;
    
    this.initialize = function(editor, drawertool) {
        this.editor = editor;
        this.drawertool = drawertool;
    };
    
    this.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();
    };

    this.hide = function() {
        this.element.style.display = 'none';
        this.focussed = false;
    };

    this.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() {
                    currnode.focus();
                }
                timer_instance.registerFunction(this, focusit, 100);
                return;
            }
            currnode = iterator.next();
        }
    }
};

function LinkDrawer(elementid, tool, wrap) {
    /* Link drawer */
    this.element = getFromSelector(elementid);
    this.tool = tool;
    function wrap(id, tag) {
        return '#'+this.element.id+' '+tag+'.'+id;
    }
    var input = getBaseTagClass(this.element, 'input', 'kupu-linkdrawer-input');
    var preview = getBaseTagClass(this.element, 'iframe', 'kupu-linkdrawer-preview');

    this.createContent = function() {
        /* display the drawer */
        var currnode = this.editor.getSelectedNode();
        var linkel = this.editor.getNearestParentOfType(currnode, 'a');
        input.value = "";
        this.preview();
        if (linkel) {
            input.value = linkel.getAttribute('href');
        } else {
            input.value = 'http://';
        };
        this.element.style.display = 'block';
        this.focusElement();
    };

    this.save = function() {
        /* add or modify a link */
        this.editor.resumeEditing();
        var url = input.value;
        var target = '_self';
        if (this.target) target = this.target;
        this.tool.createLink(url, null, null, target);
        input.value = '';

        // 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();
    };
    
    this.preview = function() {
        preview.src = input.value;
        if (this.editor.getBrowserName() == 'IE') {
            preview.width = "800";
            preview.height = "365";
            preview.style.zoom = "60%";
        };
    }
    this.preview_loaded = function() {
        if (input.value  != preview.src) {
            input.value = preview.src;
        }
    }
};

LinkDrawer.prototype = new Drawer;

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');

        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.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) {
    /* 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.init = function(tool, xsluri, libsuri, searchuri, baseelement) {
        /* 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';

        if (baseelement) {
            this.baseelement = getFromSelector(baseelement);
        } else {
            this.baseelement = getBaseTagClass(document.body, 'div', 'kupu-librarydrawer-parent');
        }

        this.tool = tool;
        this.element = document.getElementById(this.drawerid);
        if (!this.element) {
            var 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.shared.libsuri = libsuri;
        this.shared.searchuri = searchuri;
        
        // 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.shared.xmldata = null;

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

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

        // load the xsl and the initial xml
        var wrapped_callback = new ContextFixer(this._libsXslCallback, this);
        this._loadXML(this.shared.xsluri, wrapped_callback.execute);
    };

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

        // 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("", "drawertype", this.drawertype);
            xsltproc.setParameter("", "drawertitle", this.drawertitle);
            xsltproc.setParameter("", "showupload", this.showupload);
            if (this.editor.config.captions) {
                xsltproc.setParameter("", "usecaptions", 'yes');
            }
        } catch(e) {
            return; // No XSLT Processor, maybe IE 5.5?
        }
    };

    this.createContent = function() {
        // 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.shared.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 {
            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 */
        var wrapped_callback = new ContextFixer(this._libsContentCallback, this);
        this._loadXML(this.shared.libsuri, wrapped_callback.execute);
    };

    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.shared.xmldata = dom;
        this.shared.xmldata.setProperty("SelectionLanguage", "XPath");

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

    this.initialSelection = function() {
        var libnode_path = '/libraries/library[@selected]';
        var libnode = this.shared.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;
        };
        try {
            this.shared.xsltproc.setParameter("", "showupload", this.showupload);
        } catch(e) {};
        var doc = this._transformXml();
        var sourcenode = doc.selectSingleNode('//*[@id="'+id+'"]');
        var targetnode = document.getElementById(id);
        sourcenode = document.importNode(sourcenode, true);
        Sarissa.copyChildNodes(sourcenode, targetnode);
        if (!this.focussed) {
            this.focusElement();
        }

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

    this.deselectActiveCollection = function() {
        /* Deselect the currently active collection or library */
        while (1) {
            // deselect selected DOM node
            var selected = this.shared.xmldata.selectSingleNode('//*[@selected]');
            if (!selected) {
                return;
            };
            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.shared.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.updateDisplay(this.resourcespanelid);
            this.updateDisplay(this.propertiespanelid);
        } 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
            var wrapped_callback = new ContextFixer(this._libraryContentCallback, this);
            this._loadXML(src_uri, wrapped_callback.execute, null);
            this.shared.newimages = null;
        };
        // instead of running the full transformations again we get a 
        // reference to the element and set the classname...
        var newseldiv = document.getElementById(id);
        newseldiv.className = 'kupu-libsource-selected';
    };

    this._libraryContentCallback = function(dom, src_uri) {
        /* 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.shared.xmldata.selectSingleNode('//*[@selected]');
        var itemsnode = libnode.selectSingleNode("items");
        var newitemsnode = dom.selectSingleNode("//items");

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

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

    /*** Load a collection ***/

    this.selectCollection = function(id) {
        this.deselectActiveCollection();

        // First turn off current selection, if any
        this.removeSelection();
        
        var leafnode_path = "//collection[@id='" + id + "']";
        var leafnode = this.shared.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/collection[@id='" + loadedInNode + "']";
            var collnode = this.shared.xmldata.selectSingleNode(collnode_path);
            if (collnode) {
                collnode.setAttribute('selected', '1');
                this.updateDisplay(this.resourcespanelid);
                this.updateDisplay(this.propertiespanelid);
                return;
            };
        };

        // Case 2: We've already loaded the data, but there hasn't
        // been a reference made yet. So, make one :)
        uri = leafnode.selectSingleNode('uri/text()').nodeValue;
        uri = (new String(uri)).strip(); // needs kupuhelpers.js
        var collnode_path = "/libraries/collection/uri[text()='" + uri + "']/..";
        var collnode = this.shared.xmldata.selectSingleNode(collnode_path);
        if (collnode) {
            id = collnode.getAttribute('id');
            leafnode.setAttribute('loadedInNode', id);
            collnode.setAttribute('selected', '1');
            this.updateDisplay(this.resourcespanelid);
            this.updateDisplay(this.propertiespanelid);
            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');
        var src_uri = leafnode.selectSingleNode('src/text()').nodeValue;
        src_uri = src_uri.strip(); // needs kupuhelpers.js
        var wrapped_callback = new ContextFixer(this._collectionContentCallback, this);
        this._loadXML(src_uri, wrapped_callback.execute, 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.
        date = new Date();
        time = date.getTime();

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

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

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

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

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

    this.reloadCurrent = function() {
        // Reload current collection or library
        this.showupload = '';
        var current = this.shared.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

        var wrapped_callback = new ContextFixer(this._libraryContentCallback, this);
        this._loadXML(src_uri, wrapped_callback.execute);
    };

    this.removeSelection = function() {
        // turn off current selection, if any
        var oldselxpath = '/libraries/*[@selected]//resource[@selected]';
        var oldselitem = this.shared.xmldata.selectSingleNode(oldselxpath);
        if (oldselitem) {
            oldselitem.removeAttribute("selected");
        };
        if (this.selecteditemid) {
            var item = document.getElementById(this.selecteditemid);
            if (item) {
                var span = item.getElementsByTagName('span');
                if (span.length > 0) {
                    span = span[0];
                    span.className = span.className.replace(' selected-item', '');
                }
            }
            this.selecteditemid = '';
        }
        this.showupload = '';
    }

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

    this.selectItem = function (item, id) {
        /* 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 newselxpath = '/libraries/*[@selected]//resource[@id="' + id + '"]';
        var newselitem = this.shared.xmldata.selectSingleNode(newselxpath);
        newselitem.setAttribute("selected", "1");
        //this.updateDisplay(this.resourcespanelid);
        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];
            span.className += ' selected-item';
        }
        this.selecteditemid = id;
        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 */
        var searchvalue = getFromSelector('kupu-searchbox-input').value;
        //XXX make search variable configurable
        var body = 'SearchableText=' + escape(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.shared.searchuri.split('?');
        var searchuri = chunks[0];
        if (chunks[1]) {
            body += "&" + chunks[1];
        };
        var wrapped_callback = new ContextFixer(this._searchCallback, this);
        this._loadXML(searchuri, wrapped_callback.execute, 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.
        date = new Date();
        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 {
            this.shared.xmldata.importNode(resultlib, true);
        }
        var libraries = this.shared.xmldata.selectSingleNode("/libraries");
        libraries.appendChild(resultlib);

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

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

    /*** Auxiliary methods ***/

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

    this._loadXML = function(uri, callback, body) {
        /* 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 = 'GET';
        if (body) {
          method = 'POST';
        } else {
          // be sure that body is null and not an empty string or
          // something
          body = null;
        };
        xmlhttp.open(method, uri, true);
        // use ContextFixer to wrap the Sarissa callback, both for isolating 
        // the 'this' problem and to be able to pass in an extra argument 
        // (callback)
        var wrapped_callback = new ContextFixer(this._sarissaCallback, xmlhttp,
                                                callback, uri);
        xmlhttp.onreadystatechange = wrapped_callback.execute;
        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');
        };
        xmlhttp.send(body);
    };

    this._sarissaCallback = function(user_callback, uri) {
        /* 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
        */
        var errmessage = 'Error loading XML: ';
        if (uri) {
            errmessage = 'Error loading ' + uri + ':';
        };
        if (this.readyState == 4) {
            if (this.status && this.status != 200) {
                alert(errmessage + this.status);
                throw "Error loading XML";
            };
            var dom = this.responseXML;
            user_callback(dom, uri);
        };
    };
};

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

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

    this.drawertitle = "Insert Image";
    this.drawertype = "image";
    this.showupload = '';
    if (tool) {
        this.init(tool, xsluri, libsuri, searchuri, baseelement);
    }
 
    
    // upload, on submit/insert press
    this.uploadImage = function() {
        var form = document.kupu_upload_form;
        if (!form || form.node_prop_image.value=='') return;

        if (form.node_prop_caption.value == "") {
            alert("Please enter a title for the image you are uploading");
            return;        
        };
        
        var targeturi =  this.shared.xmldata.selectSingleNode('/libraries/*[@selected]/uri/text()').nodeValue
        document.kupu_upload_form.action =  targeturi + "/kupuUploadImage";
        document.kupu_upload_form.submit();
    };
    
    // called for example when no permission to upload for some reason
    this.cancelUpload = function(msg) {
        var s = this.shared.xmldata.selectSingleNode('/libraries/*[@selected]');     
        s.removeAttribute("selected");
        this.updateDisplay();
        if (msg != '') {
            alert(msg);
        };
    };
    
    // called by onLoad within document sent by server
    this.finishUpload = function(url) {
        this.editor.resumeEditing();
        var imgclass = 'image-inline';
        if (this.editor.config.captions) {
            imgclass += " captioned";
        };
        this.tool.createImage(url, null, 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.shared.xmldata.selectSingleNode(selxpath);
        
        // If no image resource is selected, check for upload
        if (!selnode) {
            var uploadbutton = this.shared.xmldata.selectSingleNode("/libraries/*[@selected]//uploadbutton");
            if (uploadbutton) {
                this.uploadImage();
            };
            return;
        };

        var uri = selnode.selectSingleNode('uri/text()').nodeValue;
        uri = uri.strip();  // needs kupuhelpers.js
        var alt = getFromSelector('image_alt').value;

        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";
        };

        this.tool.createImage(uri, alt, imgclass);
        this.drawertool.closeDrawer();
    };
};

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

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

    this.drawertitle = "Insert Link";
    this.drawertype = "link";
    this.showupload = '';
    if (tool) {
        this.init(tool, xsluri, libsuri, searchuri, baseelement);
    }

    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.shared.xmldata.selectSingleNode(selxpath);
        if (!selnode) {
            return;
        };

        var uri = selnode.selectSingleNode('uri/text()').nodeValue;
        uri = uri.strip();  // needs kupuhelpers.js
        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 type = null;
        var name = getFromSelector('link_name').value;
        var target = null;
        if (getFromSelector('link_target') && getFromSelector('link_target').value != '')
            target = getFromSelector('link_target').value;
        
        this.tool.createLink(uri, type, name, target, title);
        this.drawertool.closeDrawer();
    };
};

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

/* Function to suppress enter key in drawers */
function HandleDrawerEnter(event, clickid) {
    var key;
    event = event || window.event;
    key = event.which || event.keyCode;

    if (key==13) {
        if (clickid) {
            var button = document.getElementById(clickid);
            if (button) {
                button.click();
            }
        }
        event.cancelBubble = true;
        if (event.stopPropogation) event.stopPropogation();

        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.
 *
 *****************************************************************************/

// $Id: kupuploneinit.js 21183 2005-12-15 15:19:35Z duncan $

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

    var iframe = getFromSelector(prefix+'iframe.kupu-editor-iframe');
    var textarea = getFromSelector(prefix+'textarea.kupu-editor-textarea');
    var l = new DummyLogger();

    // XXX this should be fixed in stylesheets, but I don't know how to do 
    // that without applying this change to the outter document. Damn iframes.
    var ibody = iframe.contentWindow.document.body;
    var form = textarea.form;
    ibody.innerHTML = textarea.value || '<p class=""><br></p>';

    // now some config values
    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 = new KupuEditor(doc, conf, l);

    // add the contextmenu
    var cm = new ContextMenu();
    kupu.setContextMenu(cm);

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

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

    var boldchecker = ParentWithStyleChecker(new Array('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(new Array('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);

    var underlinechecker = ParentWithStyleChecker(new Array('u'));
    var underlinebutton = new KupuStateButton(prefix+'button.kupu-underline', 
                                              execCommand('underline'),
                                              underlinechecker,
                                              'kupu-underline', 
                                              'kupu-underline-pressed');
    kupu.registerTool('underlinebutton', underlinebutton);

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

    var superscriptchecker = ParentWithStyleChecker(new Array('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 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 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);

    // Use the generic beforeUnload handler if we have it:
    var beforeunloadTool = window.onbeforeunload && window.onbeforeunload.tool;
    if (beforeunloadTool) {
        var initialBody = ibody.innerHTML;
        beforeunloadTool.addHandler(function() {
            return ibody.innerHTML != initialBody;
        });
        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;
        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 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();
    kupu.registerTool('drawertool', drawertool);

    var drawerparent = prefix+'div.kupu-librarydrawer-parent';
    var linklibdrawer = new LinkLibraryDrawer(linktool, conf['link_xsl_uri'],
                                              conf['link_libraries_uri'],
                                              conf['search_links_uri'], drawerparent);
    drawertool.registerDrawer(prefix+'linklibdrawer', linklibdrawer, kupu);

    var imagelibdrawer = new ImageLibraryDrawer(imagetool, conf['image_xsl_uri'],
                                                conf['image_libraries_uri'],
                                                conf['search_images_uri'], drawerparent);
    drawertool.registerDrawer(prefix+'imagelibdrawer', imagelibdrawer, kupu);

    var linkdrawer = new LinkDrawer(prefix+'div.kupu-linkdrawer', linktool);
    drawertool.registerDrawer(prefix+'linkdrawer', linkdrawer, 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);

    return kupu;
};

// modify LinkDrawer so all links have a target
// defaults to _self, override here if reqd.
//LinkDrawer.prototype.target = '_blank';

