/* XHTMLGenerator (alias Element.toXHTML((id||element)[, language][, encoding][, namespaces])) * * Creates well-formed XML as HTML and tries to make valid XHTML * XHTMLGenerator is a helper class for Miller * * Todo * - Better commenting * - Every TODO in this document * - If a Javascript QA is made: make QA */ var XHTMLGenerator = Class.create(); XHTMLGenerator.prototype = { /* initialize * * sets all default values and starts looping through the children of the given element */ initialize : function(element, language, encoding, namespaces) { if (!(element = $(element))) throw Error("element has to be a html node object or the document object"); this.currentNode = element; this.language = this.checkLanguageCode(language) || "nl-NL"; this.encoding = encoding || "UTF-8"; this.namespaces = namespaces || {"xmlns" : "http://www.w3.org/1999/xhtml"}; this.attributeDefaults = this.getAttributeDefaults(); this.nodeStack = new Array(); this.xhtml = ""; $A(this.currentNode.childNodes).each(this.handleNode.bind(this)); }, /* getAttributeDefaults * * returns the tags that have required attributes, with the attribute name and a default value */ getAttributeDefaults : function(){ return {"applet" : {"width" : "100px", "height" : "100px"}, "area" : {"id" : "generate-id()", "alt" : ""}, "basefont" : {"size" : "12px"}, "bdo" : {"dir" : "ltr"}, "form" : {"action" : "/"}, "img" : {"alt" : "", "src" : ""}, "map" : {"id" : "generate-id()"}, "meta" : {"content" : ""}, "optgroup" : {"label" : ""}, "param" : {"name" : ""}, "script" : {"type" : ""}, "style" : {"type" : ""}, "textarea" : {"rows" : "3", "cols" : "50"}}; }, /* getRandomId * * generates a random id */ getRandomId : function(){ return "id"+Math.floor(Math.random()*100000001); }, /* handleNode * * checks the type of the current node and handles accordingly */ handleNode : function(node){ this.currentNode = node; switch (this.currentNode.nodeType) { case 1: this.handleElement(); break; case 3: this.handleText(); break; case 8: this.handleComment(); break; default: break; } }, /* handleElement * * binds all functions to handle an element */ handleElement : function(){ this.currentNodeName = String(this.currentNode.tagName).toLowerCase(); if(this.doNotHandle()) return; if(this.currentNodeName == 'html') this.handleHTMLElement(); this.xhtml += '<'+this.currentNodeName; this.addMissingRequiredAttributes(); $A(this.currentNode.attributes).each(this.handleAttribute.bind(this)); if (this.currentNode.canHaveChildren || this.currentNode.hasChildNodes()) this.handleChildren(); else this.handleChildless(); }, /* doNotHandle * * checks if the current node must be skipped */ doNotHandle : function(){ if (this.currentNodeName == '') return true; else if (this.currentNodeName == 'meta' && String(this.currentNode.name).toLowerCase() == 'generator') return true; else if (this.currentNodeName == '!') return true; else return false; }, /* handleHTMLElement * * does special actions on the html element */ handleHTMLElement : function(){ if (this.currentNodeName == 'html'){ this.xhtml += '\n\n'; if(!this.currentNode.getAttribute('lang')) this.currentNode.setAttribute('lang', this.language); if(!this.currentNode.getAttribute('xml:lang')) this.currentNode.setAttribute('xml:lang', this.language); $H(this.namespaces).each(this.addNamespaces.bind(this)); return false; } }, /* addNamespaces * * adds the given or default namespace(s) to the html node */ addNamespaces : function(namespace){ this.currentNode.setAttribute(namespace.key, namespace.value); }, /* addMissingRequiredAttributes * * adds required attributes to the node if they are missing * uses the default value from attributeDefaults */ addMissingRequiredAttributes : function(){ $H(this.attributeDefaults[this.currentNodeName]).each(function(attribute){ if(!this.currentNode.getAttribute(attribute.key)){ if(attribute.value == "generate-id()") this.currentNode.setAttribute(attribute.key, this.getRandomId()); else this.currentNode.setAttribute(attribute.key, attribute.value); } }.bind(this)); }, /* handleChildren * * handles the childnodes of the current node */ handleChildren : function(){ this.xhtml += '>'; this.nodeStackPush(this.currentNode); $A(this.currentNode.childNodes).each(this.handleNode.bind(this)); this.currentNode = this.nodeStackPop(); this.currentNodeName = String(this.currentNode.tagName).toLowerCase(); this.xhtml += ''; }, /* handleChildless * * handles nodes tha don't have children or are not allowed to have children */ handleChildless : function() { if (this.currentNodeName == 'style' || this.currentNodeName == 'title' || this.currentNodeName == 'script' || this.currentNodeName == 'param') this.childlessTextContent(); else this.xhtml += ' />'; }, /* childlessTextContent * * this function will only be used in IE, the property canHaveChildren is true in Firefox for all three tested nodes */ childlessTextContent : function(){ this.xhtml += '>'; if(this.currentNodeName == 'script') nodeContent = this.currentNode.text; else nodeContent = this.currentNode.innerHTML; if (this.currentNodeName == 'style') nodeContent = String(nodeContent).replace(/[\n]+/g,'\n'); this.xhtml += nodeContent+''; }, /* nodeStackPush * * pushes node to the nodeStack * nodeStack is used to keep track of the currently handled node node and the previous nodes which still need handling */ nodeStackPush : function(node){ this.nodeStack.push(node); }, /* nodeStackPop * * pops of the last added node from nodeStack and returns it */ nodeStackPop : function(node){ return this.nodeStack.pop(); }, /* handleText * * cleans up textnodes * todo: full character encoding */ handleText : function(){ if (this.currentNode.nodeValue == '\n'){ return; } this.xhtml += String(this.currentNode.nodeValue).replace(/\n{2,}/g, "\n").replace(/\&/g, "&").replace(//g, ">").replace(/\u00A0/g, " "); }, /* handleComment * * removes all dubble hyphens and adds a trailing space if the comment ends with a hyphen * returns nothing if IE 5.0/5.5 comment is used * adds the result to this.xhtml */ handleComment : function(){ if(new RegExp().compile("^"; }, /* handleAttribute * * adds attributes */ handleAttribute : function(attribute){ var attributeName = attribute.nodeName.toLowerCase(); if(!attribute.specified && (attributeName != 'selected' || !this.currentNode.selected) && (attributeName != 'style' || this.currentNode.style.cssText == '') && attributeName != 'value') return; if(attributeName == '_moz_dirty' || attributeName == '_moz_resizing' || this.currentNodeName == 'br' && attributeName == 'type' && this.currentNode.getAttribute('type') == '_moz') return; attributeOk = true; switch (attributeName) { case "style": attributeValue = this.currentNode.style.cssText; break; case "class": attributeValue = this.currentNode.className; break; case "http-equiv": attributeValue = this.currentNode.httpEquiv; break; case "noshade": case "checked": case "selected": case "multiple": case "nowrap": case "disabled": attributeValue = attributeName; break; default: try { attributeValue = this.currentNode.getAttribute(attributeName, 2); } catch (e) { attributeOk = false; } break; } if (attributeOk) this.xhtml += ' '+attributeName+'="'+String(attributeValue).replace(/\&/g, "&").replace(//g, ">").replace(/\"/g, """)+'"'; }, /* result * * returns the complete result of the parsed node */ result : function(){ return this.xhtml; }, /* checkLanguageCode * * checks the language code for the correct format */ checkLanguageCode : function(language){ if(new RegExp().compile("^[a-z]{2}-[A-Z]{2}$").exec(language)) return language; else return; } }; if (window.Element && window.XHTMLGenerator) { Object.extend(Element, { toXHTML: function(element, language, encoding, namespaces){ return new XHTMLGenerator(element, language, encoding, namespaces).result(); } }); }