/* 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 += ''+this.currentNodeName+'>';
},
/* 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+''+this.currentNodeName+'>';
},
/* 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();
}
});
}