
// XHTML library
// Adds functions innerXHTML(el) and outerXHTML(el),
// and, where supported (Firefox, Safari), adds
// .innerXHTML() and .outerXHTML() to element prototypes
//
// Version: 0.1
// 
// Author: Jon Davis <jon@jondavis.net>
// Please contact the author for feedback.
//

if (!window.xhtmlFormatting) {
	window.xhtmlFormatting = "formatted";
}

if (String.prototype && !String.prototype.trim) {
    String.prototype.trim = function() {
        return this.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
    }
}

function innerXHTML(el, strict) {
    var e;
    var ret = '';
    for (e=0; e<el.childNodes.length; e++) {
        var childNode = el.childNodes.item(e);
        ret += outerXHTML(childNode, strict);
        if (xhtmlFormatting == "formatted" &&
            !childNode.tagName && 
          e < el.childNodes.length - 1 && 
          el.childNodes[e+1].tagName && 
          tagIsLineBreaking(el.childNodes[e+1].tagName)) {
            ret += "\n";
        }
    }
    return ret.replace(/\s*\n\s*\n/g, '\n');
}

function outerXHTML(el, strict, tab) {
    var tagName = el.tagName;
    var attributes = el.attributes;
    var childNodes = el.childNodes;
    var supportsInlineTerminator = true;
    
    if (tagName) {
        switch(tagName.toLowerCase()) {
            case "script":
            case "link":
                supportsInlineTerminator = false;
                break;
            default: 
                supportsInlineTerminator = true;
                break;
        }
    }
    
    var ret = "";
    var t;
    if (!tab) tab = 0;
    for (t=0; t<tab; t++) {
        ret += "\t";
    }
    ret += "<";
    if (tagName && (!strict || tagInXhtml(tagName.toLowerCase()))) {
        var i;
        ret += tagName.toLowerCase();
        if (attributes) {
            var styleTagUsed = false;
            for (i=0; i<attributes.length; i++) {
                var attribName = attributes.item(i).nodeName;
                var attribValue = attributes.item(i).value;
                if (attribValue != null &&
                    attribValue != "null" &&
                    attribValue.trim() != "" &&
                    attribValue != "inherit" &&
                    (!strict || attribInXhtml(tagName, attribName, attribValue))) {
                    if (attribName.toLowerCase() == "style") {
                        styleTagUsed = true;
                    }
                    ret += " " + attribName.toLowerCase()
                        + "=\"" + xmlEncode(attribValue)
                        + "\"";
                }
            }
            if (!styleTagUsed && attribInXhtml(tagName, "style") &&
                el.style.cssText) {
                var styleText = el.style.cssText;
                var altStyleText = "";
                var styleElements = styleText.split(/\;/);
                for (s=0; s<styleElements.length; s++) {
                    var styleElement = styleElements[s].split(/:/);
                    altStyleText += styleElement[0].toLowerCase()
                        + ":" 
                        + styleElement[1]
                        + ";";                  
                }
                //ret += " style=\"" + styleText + "\"";
                ret += " style=\"" + altStyleText + "\"";
            }
        }
        if (childNodes && childNodes.length > 0) {
            ret += ">";
            var childTags = false;
            var prevWasTag = false;
            for (i=0; i<childNodes.length; i++) {
                var tabv = tab+1;
                var cv = "";
                if (!childNodes.item(i).tagName) {
                    tabv = null;
                } else {
                    if (xhtmlFormatting == "formatted" &&
                        childNodes.item(i).childNodes && 
                        childNodes.item(i).childNodes.length > 0){
                        cv = "\n";
                        childTags = true;
                    } else {
                        tabv = null;
                    }
                }
                cv += outerXHTML(childNodes.item(i), strict, tabv);
                if (xhtmlFormatting == "formatted" &&
                  cv.substr(0, 1) == "<" && prevWasTag) {
                    ret += "\n";
                } 
                ret += cv;
                if (xhtmlFormatting == "formatted" && (
                  childNodes.item(i).tagName != undefined) && 
                  tagIsLineBreaking(childNodes.item(i).tagName)) {
                    ret += "\n";
                    var t;
                    for (t=0; t<tab; t++) {
                        ret += "\t";
                    }
                }
                prevWasTag = (childNodes.item(i).tagName != undefined);
            }
            if (xhtmlFormatting == "formatted" && childTags) {
                ret += "\n";
                for (i=0; i<tab;i++) {
                    ret += "\t";
                }
            } 
            ret += "</" + tagName.toLowerCase() + ">";
        } else {
            if (supportsInlineTerminator) {
                ret += " />";
            } else {
                ret += ">";
                if (xhtmlFormatting == "formatted") {
					ret += "\n";
					for (i=0; i<tab;i++) {
						ret += "\t";
					}
				}
                ret += "</" + tagName.toLowerCase() + ">";
            }
        }
        if (xhtmlFormatting == "formatted" && tagName && 
          tagName.toLowerCase() != "br" && tagIsLineBreaking(tagName)) {
            ret += "\n";
            for (i=0; i<tab;i++) {
                ret += "\t";
            }
        }
        return ret.replace(/\s*\n(\t*)\s*\n/g, '\n$1');
    } else {
        // text
        if (el.nodeValue) {
            return el.nodeValue.trim(); 
        } else {
            return el.toString().trim(); 
        }
    }    
}

function xmlAttributeEncode(str) { // todo
    return xmlEncode(str);
}

function xmlEncode(str) { // todo
    return str.replace(/&/g, "&amp;").replace(/\"/g, "&quot;");
}

function tagIsLineBreaking(tagName) {
    if (!tagName) return false;
    switch (tagName.toLowerCase()) {
        case "h1":
        case "h2":
        case "h3":
        case "h4":
        case "h5":
        case "h6":
        case "br":
        case "p":
        case "div":
        case "table":
        case "blockquote":
        case "ul":
        case "ol":
            return true;
    }
}

function tagInXhtml(tag) {
    switch(tag.toLowerCase()) {
        case "html":
        case "head":
        case "base":
        case "meta":
        case "title":
        case "script":
        case "style":
        case "link":
        case "body":
        
        case "iframe":
        case "frameset":
        case "frame":
        case "a":
        case "area":
        case "b":
        case "big":
        case "blockquote":
        case "br":
        case "center":
        case "cite":
        case "code":
        case "col":
        case "colgroup":
        case "dd":
        case "del":
        case "div":
        case "dl":
        case "em":
        case "font":
        case "h1":
        case "h2":
        case "h3":
        case "h4":
        case "h5":
        case "h6":
        case "hr":
        case "i":
        case "iframe":
        case "img":
        case "ins":
        case "li":
        case "map":
        case "ol":
        case "p":
        case "pre":
        case "q":
        case "span":
        case "small":
        case "strong":
        case "sub":
        case "sup":
        case "table":
        case "tbody":
        case "td":
        case "tfoot":
        case "th":
        case "thead":
        case "tr":
        case "u":
        case "ul":
            return true;
        default:
            return false;
    }
}

function attribInXhtml(tag, attrib, value) {
    //if (attrib != attrib.toLowerCase()) return false;
    attrib = attrib.toLowerCase();
    if (attrib == "disabled" && value == "false") return false;
    
    switch (attrib) {
    
        // core attributes
        case "id":
        case "class":
        case "style":
        case "title":
            return tagInXhtml(tag);
    
        // event attributes
        case "onclick":
        case "ondblclick":
        case "onmousedown":
        case "onmouseup":
        case "onmouseover":
        case "onmousemove":
        case "onmouseout":
        case "onkeydown":
        case "onkeypress":
        case "onkeyup":
            return tagInXhtml(tag);
    
        // language attributes
        case "lang":
            return tagInXhtml(tag);
        case "dir":
            switch (value) {
                case "ltr":
                case "rtl":
                    return tagInXhtml(tag);
                default:
                    return false;
            }
            break;
            
        // keyboard attributes
        case "accesskey":
        case "tabindex":
            if (value == "0") return false;
            switch (tag.toLowerCase()) {
                case "br":
                    return false;
                default:
                    return tagInXhtml(tag);
            }
    }
    switch(tag) {
        case "a":
            switch (attrib) {
                case "accesskey":
                case "charset":
                case "coords":
                case "href":
                case "hreflang":
                case "name":
                case "rel":
                case "rev":
                case "shape":
                case "tabindex":
                case "target":
                case "type":
                    return true;
                default: 
                    return false;
            }
        case "area":
            switch (attrib) {
                case "alt":
                case "coords":
                case "href":
                case "nohref":
                case "shape":
                case "target":
                    return true;
                default: 
                    return false;
            }
        case "b":
        case "big":
        case "br":
        case "center":
        case "cite":
        case "code":
        case "dd":
        case "dl":
        case "em":
        case "i":
        case "small":
        case "strong":
        case "sub":
        case "sup":
        case "u":
            return false;
        case "blockquote":
            switch (attrib) {
                case "cite":
                    return true;
                default: 
                    return false;
            }
        case "col":
        case "colgroup":
            switch (attrib) {
                case "align":
                case "char":
                case "charoff":
                case "span":
                case "valign":
                case "width":
                    return true;
                default: 
                    return false;
            }
        case "del":
            switch (attrib) {
                case "cite":
                case "datetime":
                    return true;
                default: 
                    return false;
            }
        case "font":
            switch (attrib) {
                case "color":
                case "face":
                case "size":
                    return true;
                default: 
                    return false;
            }
        case "h1":
        case "h2":
        case "h3":
        case "h4":
        case "h5":
        case "h6":
            switch (attrib) {
                case "align":
                    return true;
                default: 
                    return false;
            }
        case "hr":
            switch (attrib) {
                case "align":
                case "noshade":
                case "size":
                case "width":
                    return true;
                default: 
                    return false;
            }
        case "iframe": // todo
            return true;
        case "img":
            switch (attrib) {
                case "align":
                case "alt":
                case "border":
                case "height":
                case "hspace":
                case "ismap":
                case "longdesc":
                case "src":
                case "usemap":
                case "vspace":
                case "width":
                    return true;
                default: 
                    return false;
            }
        case "ins":
            switch (attrib) {
                case "cite":
                case "datetime":
                    return true;
                default: 
                    return false;
            }
        case "li":
            switch (attrib) {
                case "type":
                case "value":
                    return true;
                default: 
                    return false;
            }
        case "map":
            switch (attrib) {
                case "name":
                    return true;
                default: 
                    return false;
            }
        case "ol":
            switch (attrib) {
                case "compact":
                case "start":
                case "type":
                    return true;
                default: 
                    return false;
            }
        case "p":
            switch (attrib) {
                case "align":
                    return true;
                default: 
                    return false;
            }
        case "pre":
            switch (attrib) {
                case "width":
                    return true;
                default: 
                    return false;
            }
        case "q":
            switch (attrib) {
                case "cite":
                    return true;
                default: 
                    return false;
            }
        case "table":
            switch (attrib) {
                case "align":
                case "bgcolor":
                case "border":
                case "cellpadding":
                case "cellspacing":
                case "frame":
                case "rules":
                case "summary":
                case "width":
                    return true;
                default: 
                    return false;
            }
        case "thead":
        case "tbody":
        case "tfoot":
            switch (attrib) {
                case "align":
                case "char":
                case "charoff":
                case "valign":
                    return true;
                default: 
                    return false;
            }
        case "td":
        case "th":
            switch (attrib) {
                case "abbr":
                case "align":
                case "axis":
                case "bgcolor":
                case "char":
                case "charoff":
                case "colspan":
                case "headers":
                case "height":
                case "nowrap":
                case "rowspan":
                case "scope":
                case "valign":
                case "width":
                    return true;
                default: 
                    return false;
            }
        case "tr":
            switch (attrib) {
                case "align":
                case "bgcolor":
                case "char":
                case "charoff":
                case "valign":
                    return true;
                default: 
                    return false;
            }
        case "ul":
            switch (attrib) {
                case "compact":
                case "type":
                    return true;
                default: 
                    return false;
            }
        default:
            return false;
    }
}

// =======================================================================

// add to HTMLElement prototype (not supported in IE)
if (!document.all && HTMLElement && HTMLElement.prototype) {
    HTMLElement.prototype.innerXHTML = function() {
        return innerXHTML(this, true);
    }
    HTMLElement.prototype.outerXHTML = function() {
        return outerXHTML(this, true);
    }
}

// add to jQuery prototype
if (window.jQuery) {
    window.jQuery.prototype.xhtml = function(newXhtml) {
        if (newXhtml) {
            return this.html(newXhtml);
        } else {
            var i;
            var ret = '';
            for (i=0; i<this.length; i++) {
                if (i>0) i += "\n";
                ret += innerXHTML(this[i], true);
            }
            return ret;
        }
    }
}


