
/*******************************
 *  Cross-Browser XML Library
 *  Jeff Martin
 *  9/25/2006
 * ----------------------------
 *  Description:
 *      a cross-browser library for loading XML data
 *
 *  Usage:
 *		<div id="contentId"></div>
 *		<script type="text/javascript">
 *			var objXML = new XML();
 *			objXML.loadXML( "path/to/data.xml" );
 *			objXML.parseXML( "<xml>alternative load method</xml>" );
 *			objXML.writeTransformedXML( "stylesheet.xsl", "contentId" );
 *		</script>
 *******************************/
 
function XML( )
{
	// set vars
	this.xmlDoc = this.newDOM();
	this.xmlDocLoaded = false;
	this.eOutput = null;
	this.waiting = null;
	this.waitedTime = 0;
	this.xslParamNames = new Array();
	this.xslParamValues = new Array();
	
	// external events
	this.onLoad = null;
	this.onWrite = null;
	
	// hook events
	var thisObj = this;
	
	// explorer
	try
	{
		this.xmlDoc.onreadystatechange = function( ) { if( thisObj.xmlDoc.readyState == 4 ) thisObj.onXMLLoaded(); };
	}
	catch( exception ) { /* ignore errors */ }
	
	// standards
	try
	{
		this.xmlDoc.onload = function( ) { thisObj.onXMLLoaded(); };
	}
	catch( exception ) { /* ignore errors */ }
}


/* --- Methods --- */

XML.prototype.loadXML = function( url )
{
	this.xmlDoc.load( url );
	this.xmlDocLoaded = false;
}

XML.prototype.parseXML = function( text )
{
	// microsoft way
	if( window.ActiveXObject )
	{
		this.xmlDoc.loadXML( text );
		this.onXMLLoaded();
	}
	// standards way
	else if( window.DOMParser )
	{
		this.xmlDoc = new DOMParser().parseFromString( text, "text/xml" );
		this.onXMLLoaded();
	}
	else
	{
		throw new Error( "Could not parse XML" );
	}
}

XML.prototype.writeTransformedXML = function( xslUrl, id )
{
	this.writeTransformedXMLToElement( xslUrl, document.getElementById( id ) );
}

XML.prototype.writeTransformedXMLToElement = function( xslUrl, eElement, recall )
{
	// if this is the first call
	if( !recall )
	{
		this.eOutput = eElement;
		
		// show the loading message
		while( this.eOutput.hasChildNodes() )
			this.eOutput.removeChild( this.eOutput.firstChild );
		this.eOutput.appendChild( document.createTextNode( "loading" ) );
		
		// clear the timer
		if( this.waiting )
			clearTimeout( this.waiting );
		this.waitedTime = 0;
	}
	
	// wait for the XML to load
	if( !this.xmlDocLoaded )
	{
		if( this.waitedTime < 5000 )
		{
			var thisObj = this;
			this.waiting = setTimeout( function( ) { thisObj.writeTransformedXMLToElement( xslUrl, eElement, true ); }, 100 );
			this.waitedTime += 100;
			
			this.eOutput.appendChild( document.createTextNode( "." ) );
		}
		else
		{
			this.eOutput.appendChild( document.createTextNode( "Failed!" ) );
		}
		return;
	}
	
	// if the XML has errors, bail
	try
	{
		this.checkError( this.xmlDoc );
	}
	catch( exception )
	{
		return;
	}
	
	// load the xsl document
	var xslDoc = this.newDOM();
	
	// hook events
	var thisObj = this;

	// explorer
	try
	{
		xslDoc.onreadystatechange = function( ) { if( xslDoc.readyState == 4 ) thisObj.onXSLLoaded( xslDoc ); }
	}
	catch( exception ) { /* ignore errors */ }
	
	// standards
	try
	{
		xslDoc.onload = function( ) { thisObj.onXSLLoaded( xslDoc ); };
	}
	catch( exception ) { /* ignore errors */ }

	xslDoc.load( xslUrl );
}

XML.prototype.clearTransformParams = function( )
{
	// clear all params
	this.xslParamNames = new Array();
	this.xslParamValues = new Array();
}

XML.prototype.addTransformParam = function( name, value )
{
	// add a new param
	this.xslParamNames.push( name );
	this.xslParamValues.push( value  );
}


/* --- Events --- */

XML.prototype.onXMLLoaded = function( )
{
	this.checkError( this.xmlDoc );
	this.xmlDocLoaded = true;
	
	// bubble events
	if( this.onLoad )
		this.onLoad( this.xmlDoc );
}

XML.prototype.onXSLLoaded = function( xslDoc )
{
	var outDom = null;
	var outText = "";
	
	try
	{
		this.checkError( xslDoc );
		
		// give parameters to the XSL document
		for( var i=0; i<this.xslParamNames.length; i++ )
		{
			var node = xslDoc.selectSingleNode( "//xsl:template[@match = '/']/xsl:param[@name = '" + this.xslParamNames[i] + "']" );
			if( node )
				node.setAttribute( "select", this.xslParamValues[i] );
		}
		
		// microsoft way
		if( window.ActiveXObject )
		{
			// OMG, internet explorer couldn't transform to an object correctly if its life depended on it
			outText = this.xmlDoc.transformNode( xslDoc );
			/*
			this.xmlDoc.transformNodeToObject( xslDoc, tempDom );
			outDom = document.importNode( tempDom.documentElement, true );
			*/
		}
		// standards way
		else if( window.XSLTProcessor )
		{
			var processor = new XSLTProcessor();
			processor.importStylesheet( xslDoc );
			outDom = processor.transformToFragment( this.xmlDoc, document );
		}
	}
	catch( exception )
	{
		outDom = this.createMessageDOM( exception.message );
	}
	
	// clear destination
	while( this.eOutput.hasChildNodes() )
		this.eOutput.removeChild( this.eOutput.firstChild );
	
	// add the DOM to the destination element
	if( outDom != null )
	{
		this.eOutput.appendChild( outDom );
	}
	// jank code for explorer's lack of support for proper xsl translations
	else if( outText != "" )
	{
		this.eOutput.innerHTML = outText;
	}
	else
	{
		this.eOutput.appendChild( document.createTextNode( "[null]" ) );
	}
	
	// bubble events
	if( this.onWrite != null )
		this.onWrite();
}


/* --- Functions --- */

XML.prototype.newDOM = function( )
{
	// try loading XML the microsoft way
	try
	{
		return new ActiveXObject( "Microsoft.XMLDOM" );
	}
	catch( exception ) { /* ignore errors */ }
	
	// try loading XML the standards way
	try
	{
		return document.implementation.createDocument( "", "", null );
	}
	catch( exception ) { /* ignore errors */ }
	
	// your browser sucks
	throw new Error( "This browser does not support XML processing." );
}

XML.prototype.checkError = function( dom )
{
	var parseError = {
		errorCode : 0,
		reason : "",
		srcText : ""
	};
	
	// microsoft way
	if( dom.parseError )
	{
		parseError.errorCode = dom.parseError.errorCode;
		parseError.reason = dom.parseError.reason;
		parseError.srcText = dom.parseError.srcText;
	}

	// mozilla way
    if( dom.documentElement && dom.documentElement.nodeName == "parsererror" )
	{
		parseError.errorCode = 1;
		var sourceText = dom.documentElement.getElementsByTagName( "sourcetext" )[0];
		if( sourceText != null )
			parseError.srcText = sourceText.firstChild.data
    }
	
	// throw the error
	if( parseError.errorCode != 0 )
	{
		throw new Error( "Error loading XML\n" + parseError.reason + "\n(code " + parseError.errorCode + ")\nSource: " + parseError.srcText );
	}
}

XML.prototype.createMessageDOM = function( message )
{
	// copy the message locally
	var msg = message.split( "\n" );
	var i = 0;
	
	// should look like this: <i>line1<br>line2<br></i>
	var eI = document.createElement( "i" );
	for( i=0; i<msg.length; i++ )
	{
		eI.appendChild( document.createTextNode( msg[i] ) );
		eI.appendChild( document.createElement( "br" ) );
	}
	return eI;
}

// ZOMG, we need a cross-browser way to set the manipluate the text of a node
XML.setText = function( node, text )
{
	// find the first text node
	if( node.hasChildNodes() )
	{
		for( var i=0; i<node.childNodes.length; i++ )
		{
			if( node.childNodes[i].nodeType == 3 ) // 3 is text node
			{
				node.childNodes[i].nodeValue = text;
				return;
			}
		}
	}

	// no text node found? add one
	node.appendChild( document.createTextNode( text ) );
}

XML.getText = function( node )
{
	// find the first text node
	if( node.hasChildNodes() )
	{
		for( var i=0; i<node.childNodes.length; i++ )
		{
			if( node.childNodes[i].nodeType == 3 ) // 3 is the magic number for text node
			{
				return node.childNodes[i].nodeValue;
			}
		}
	}
	
	// none found
	return "";
}

// short-cut for attribute manipulation
XML.getAttribute = function( node, name )
{
	var attr = node.attributes.getNamedItem( name );
	if( attr == null )
		return null;
	else
		return attr.nodeValue;
}

XML.setAttribute = function( node, name, value )
{
	var attr = document.createAttribute( name );
	attr.nodeValue = value;
	node.setNamedItem( attr );
}



// add in some additional XML functionality for Explorer

/*
 * The following code is borrowed (and modified) from Mark Wubben under a creative commons license
 * http://neo.dzygn.com/downloads/importNode.txt
 */

if( !document.importNode )
{
	document.importNode = function(oNode, bImportChildren){
		var oNew;

		if(oNode.nodeType == 1){
			oNew = document.createElement(oNode.nodeName);
			for(var i = 0; i < oNode.attributes.length; i++){
				oNew.setAttribute(oNode.attributes[i].name, oNode.attributes[i].value);
			}
			
			// copy the special attributes correctly for IE
			for( var i = 0; i < oNode.attributes.length; i++ )
            {
                var attr = oNode.attributes[i];
				if( attr.name == "class" )
				{
					oNew.className = attr.nodeValue;
				}
            }
				
		} else if(oNode.nodeType == 3){
			oNew = document.createTextNode(oNode.nodeValue);
		}
		
		if(bImportChildren && oNode.hasChildNodes()){
			for(var oChild = oNode.firstChild; oChild; oChild = oChild.nextSibling){
				oNew.appendChild(document.importNode(oChild, true));
			}
		}
		
		return oNew;
	}
}


/*
 * The following code is borrowed from km0ti0n under a creative commons license
 * http://km0ti0n.blunted.co.uk/mozxpath/mozxpath.js
 */

// mozXPath [http://km0ti0n.blunted.co.uk/mozxpath/] km0ti0n@gmail.com
// Code licensed under Creative Commons Attribution-ShareAlike License 
// http://creativecommons.org/licenses/by-sa/2.5/
if( document.implementation.hasFeature("XPath", "3.0") )
{
	XMLDocument.prototype.selectNodes = function(cXPathString, xNode)
	{
		if( !xNode ) { xNode = this; } 

		var oNSResolver = this.createNSResolver(this.documentElement)
		var aItems = this.evaluate(cXPathString, xNode, oNSResolver, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
		var aResult = [];
		for( var i = 0; i < aItems.snapshotLength; i++)
		{
			aResult[i] =  aItems.snapshotItem(i);
		}
		
		return aResult;
	}
	XMLDocument.prototype.selectSingleNode = function(cXPathString, xNode)
	{
		if( !xNode ) { xNode = this; } 

		var xItems = this.selectNodes(cXPathString, xNode);
		if( xItems.length > 0 )
		{
			return xItems[0];
		}
		else
		{
			return null;
		}
	}

	Element.prototype.selectNodes = function(cXPathString)
	{
		if(this.ownerDocument.selectNodes)
		{
			return this.ownerDocument.selectNodes(cXPathString, this);
		}
		else{throw "For XML Elements Only";}
	}

	Element.prototype.selectSingleNode = function(cXPathString)
	{	
		if(this.ownerDocument.selectSingleNode)
		{
			return this.ownerDocument.selectSingleNode(cXPathString, this);
		}
		else{throw "For XML Elements Only";}
	}

}
