Monday, November 27, 2006

Why is sliced bread such a great thing?

I happened to be pondering why everything is compared to sliced bread (ie. "That's the greatest thing since sliced bread"), so I looked around a little and found this article to justify why everything is the greatest thing since sliced bread:

"Sliced bread was the culmination of a century of technological innovation."
-- http://www.engineerguy.com/comm/4263.htm

Wednesday, November 08, 2006

Using XML, XPath, and XSLT with JavaScript

There are several JavaScript XML libraries available to try to provide a cross-browser XML library. I've previously cited zXML in my Inline SVG article, there's also Sarissa and XML for <Script> and I'm sure others. However, when I started using them I discovered various problems and limitations with them for what I needed to do. So, I decided to write my own cross-browser XML routines. I wanted an XML library that was as lightweight as possible, doing just what I needed and utilizing as much of the built in browser capabilities as possible. In this article, I will first present a simple XML class which just holds the XML DOM object returned from an AJAX request and provides functions to get and change node values based on XPaths. Secondly, I will give a simple XSLT class to preform XSL Transformations. The complete code for the XML and XSL library is available here

XML

First off is the constructor which determined if we will be using IE (ActiveX) functions or W3C standard functions for XML manipulation and either stores the XML DOM returned as the responseXML from an XMLHttpRequest(XHR) or creates an empty DOM object for later manipulation.
//
// XML
//
function XML(xmlDom) {
this.isIE = window.ActiveXObject;
if (xmlDom != null) {
 this.xmlDom = xmlDom;
} else {
 // create an empty document
 if (this.isIE) {
    Try.these (
       function() { axDom = new ActiveXObject("MSXML2.DOMDocument.5.0"); },
       function() { axDom = new ActiveXObject("MSXML2.DOMDocument.4.0"); },
       function() { axDom = new ActiveXObject("MSXML2.DOMDocument.3.0"); },
       function() { axDom = new ActiveXObject("MSXML2.DOMDocument"); },
       function() { axDom = new ActiveXObject("Microsoft.XmlDom"); }
    );
    this.xmlDom = axDom;
 } else {
    this.xmlDom = document.implementation.createDocument("", "", null);
 }
}
};
In case you're not getting the XML data from an XHR response, I also provided a load function to load an XML file from an URL.
// load
//
// Loads an XML file from an URL
XML.prototype.load = function(url) {
 this.xmlDom.async = false;
 this.xmlDom.load(url);
};
Next, I want to be able to find a single node in the XML DOM based on an XPath. The getNode function will do that for me.
// getNode
//
// get a single node from the XML DOM using the XPath
XML.prototype.getNode = function(xpath) {
 if (this.isIE) {
    var result = this.xmlDom.selectSingleNode(xpath);
 } else {
    var evaluator = new XPathEvaluator();
    var result = evaluator.evaluate(xpath, this.xmlDom, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
 }
 return result;
};
More often, I don't need the actual DOM node object, but the value of that node, so I provide the getNodeValue a function to get the value of a node.
// getNodeValue
//
// get the value of a the element specified by the XPath in the XML DOM
XML.prototype.getNodeValue = function(xpath) {
 var value = null;
 try {
    var node = this.getNode(xpath);

    if (this.isIE &&amp; node) {
       value = node.text;
    } else if (!this.isIE && node.singleNodeValue) {
       value = node.singleNodeValue.textContent;
    }
 } catch (e) {}

 return value;
};
If you're just looking for a particular node/value, the previous functions are fine, but more often you need a group of node or node values (ex. all the book titles). So, the full JavaScript file also contains corresponding functions to get multiple nodes (getNodes) and node values as an array (getNodeValues). At some point, you'll probably want to access this XML DOM as XML data (string), so we need a function to serialize the XML from the DOM to a String. The getNodeAsXml function will do that for you. If you pass in the root XPath ("/") it will serialize the complete XML file. Also, the included prettyPrintXml function can be used to escape the HTML characters.
// getNodeAsXml
//
// get the XML contents of a node specified by the XPath
XML.prototype.getNodeAsXml = function(xpath) {
 var str = null;
 var aNode = this.getNode(xpath);
 try {
    if (this.isIE) {
       str = aNode.xml;
    } else {
       var serializer = new XMLSerializer();
       str = serializer.serializeToString(aNode.singleNodeValue);
    }
 } catch (e) {
    str = "ERROR: No such node in XML";
 }
 return str;
};
So far, we've only read the XML data we received. But we may also want to change the contents of the XML. So, here are the functions necessary to change existing node values (updateNodeValue), add new nodes/values (insertNode), and delete existing nodes (removeNode). Note: the XPaths used for node insertions cannot be overly complex as I wanted to keep this library simple.
// updateNodeValue
//
// update a specific element value in the XML DOM
XML.prototype.updateNodeValue = function(xpath, newvalue) {
 var node = this.getNode(xpath);
 var changeMade = false;
 newvalue = newvalue.trim();

 if (this.isIE &&amp; node) {
    if (node.text != newvalue) {
       node.text = newvalue;
       changeMade = true;
    }
 } else if (!this.isIE && node.singleNodeValue) {
    if (node.singleNodeValue.textContent != newvalue) {
       node.singleNodeValue.textContent = newvalue;
       changeMade = true;
    }
 } else {
    if (newvalue.length > 0) {
       this.insertNode(xpath);
       changeMade = this.updateNodeValue(xpath, newvalue);
    }
 }

 return changeMade;
};

// insertNode
//
// insert a new element (node) into the XML document based on the XPath
XML.prototype.insertNode = function(xpath) {
 var xpathComponents = xpath.split("/");
 var newChildName = xpathComponents.last();
 var parentPath = xpath.substr(0, xpath.length - newChildName.length - 1);
 var qualifierLoc = newChildName.indexOf("[");
 // remove qualifier for node being added
 if (qualifierLoc != -1) {
    newChildName = newChildName.substr(0, qualifierLoc);
 }
 var node = this.getNode(parentPath);
 var newChild = null;
 if (this.isIE && node) {
    newChild = this.xmlDom.createElement(newChildName);
    node.appendChild(newChild);
 } else if ((!this.isIE) && node.singleNodeValue) {
    newChild = this.xmlDom.createElement(newChildName);
    node.singleNodeValue.appendChild(newChild);
 } else {
    // add the parent, then re-try to add this child
    var parentNode = this.insertNode(parentPath);
    newChild = this.xmlDom.createElement(newChildName);
    parentNode.appendChild(newChild);
 }
 return newChild;
};

// removeNode
//
// remove an element (node) from the XML document based on the xpath
XML.prototype.removeNode = function(xpath) {
 var node = this.getNode(xpath);
 var changed = false;
 if (this.isIE &&amp; node) {
    node.parentNode.removeChild(node);
    changed = true;
 } else if ((!this.isIE) && node.singleNodeValue) {
    node.singleNodeValue.parentNode.removeChild(node.singleNodeValue);
    changed = true;
 }
 return changed;
};
You should now be able to fully read and manipulate XML using XPaths. For any significant reformatting or processing of the XML, you'll probably want to utilize XSLT. So, next I'll provide a simple XSLT class. It will preload (into a DOM object) an XSL file in its constructor. This provides rapid transformations of new XML data passed in to it's transform function (including parameters). First, the constructor which requires an URL pointing to the XSL file to be used for transformations. It does not accept a DOM object like the XML class did as it is assumed that the XSL (layout information) is more-or-less static and stored in a file whereas the XML data would most likely be dynamic and received via an XHR request.
//
// XSLT Processor
//
function XSLT(xslUrl) {
 this.isIE = window.ActiveXObject;
 if (this.isIE) {
    var xslDom = new ActiveXObject("MSXML2.FreeThreadedDOMDocument");
    xslDom.async = false;
    xslDom.load(xslUrl);
    if (xslDom.parseError.errorCode != 0) {
       var strErrMsg = "Problem Parsing Style Sheet:\n" +
          " Error #: " + xslDom.parseError.errorCode + "\n" +
          " Description: " + xslDom.parseError.reason + "\n" +
          " In file: " + xslDom.parseError.url + "\n" +
          " Line #: " + xslDom.parseError.line + "\n" +
          " Character # in line: " + xslDom.parseError.linepos + "\n" +
          " Character # in file: " + xslDom.parseError.filepos + "\n" +
          " Source line: " + xslDom.parseError.srcText;
         alert(strErrMsg);
       return false;
    }
    var xslTemplate = new ActiveXObject("MSXML2.XSLTemplate");
    xslTemplate.stylesheet = xslDom;
    this.xslProcessor = xslTemplate.createProcessor();
 } else {
    var xslDom = document.implementation.createDocument("", "", null);
    xslDom.async = false;
    xslDom.load(xslUrl);
    this.xslProcessor = new XSLTProcessor();
    this.xslProcessor.importStylesheet(xslDom);
}
};
Finally, the transform function will preform the XSL transformation and return the result. It accepts parameters to be passed to the XSL as an associative array of parameter name and value pairs.
// transform
//
// Transform an XML document
XSLT.prototype.transform = function(xml, params) {
 // set stylesheet parameters
 for (var param in params) {
    if (typeof params[param] != 'function') {
       if (this.isIE) {
          this.xslProcessor.addParameter(param, params[param]);
       } else {
          this.xslProcessor.setParameter(null, param, params[param]);
       }
    }
 }

 if (this.isIE) {
    this.xslProcessor.input = xml.xmlDom;
    this.xslProcessor.transform();
    var output = this.xslProcessor.output;
 } else {
    var resultDOM = this.xslProcessor.transformToDocument(xml.xmlDom);
    var serializer = new XMLSerializer();
    var output = serializer.serializeToString(resultDOM);
 }
 return output;
};
You should now be able to easily utilize XML and XSL on your web pages. An example usage of this XML/XSLT library would be to first build an XSLT object during the page initialization (onload).
xslProcessor = new XSLT(xslUrl);
Then, when XML responses are received from an AJAX request, process them. For example,
// get the XML data
try {
 var xmlData = new XML(request.responseXML);
} catch (e) {
 alert(request.responseText);
}

// get the username out of the XML
var userName = xmlData.getNodeValue('//userName');

// transform the XML to some HTML content
var newData = xslProcessor.transform(xmlData, {'param':'value'});
document.getElementById('someDiv').innerHTML = newData;
Update (February 27, 2007): I previously had a function in my XML class to generate valid HTML for the XML, but I've now modified it to use Oliver Becker's XML to HTML Verbatim Formatter with Syntax Highlighting stylesheet to format the XML. If that is not available, it will default to simply serialize the XML and convert the special characters. The code for this looks like:
/ Define a class variable which can be used to apply a style sheet to
// the XML for format it to HTML
XML.htmlFormatter = new XSLT("xsl/xmlverbatim.xsl");

// toHTML
//
// Transform the XML into formatted HTML
//
XML.prototype.toHTML = function() {
var html = null;
if (XML.htmlFormatter) {
 html = XML.htmlFormatter.transform(this);
} else {
 html = this.getNodeAsXml('/');
 if (html != null) {
  html = html.replace(/&/g, "&");
  html = html.replace(/</g, "&lt;");
  html = html.replace(/>/g, "&gt;<br/>");
 }
}
   return html;
}
As before, the complete package is available to download Update (March 7, 2007): I have updated this package to utilize Google's AJAXSLT XSL-T implementation when a native JavaScript XSLT function is not available. I need to test it more before releasing it however.

Thursday, October 26, 2006

Cross-browser inline SVG solved

In three previous posts (1, 2, 3) I've discussed getting browser-side dynamically generated SVGs to render properly across browsers. I now have a solution which is working for me.

Below is a screenshot my test application now. The XML used as sample data and the XSLT to product the SVG data was provided earlier.



(Note: I was also able to add some linking between the SVG and HTML so that as you mouse over each bar it will be highlighted in the SVG and simultaneously highlighted the corresponding row in the HTML table.)

In order to make this work I used the embed tag for IE and the object tag for all other browsers (tested with Firefox and Opera). My HTML has a div named svgDiv which will dynamically be given the SVG to render. For Firefox I can create my object with the dynamic SVG in one step, but for Internet Explorer, I first have to embed a static, blank SVG (as mentioned earlier) and then replace the blank SVG with the dynamic SVG. However, I determined that this could not be done all in one JavaScript function. I found that if I programatically add the embedded blank SVG, and them immediately replace the blank SVG with the dynamic one, it would fail, but having something like an alert message between adding the blank SVG and replacing it with the dynamic SVG would work. The only explanation I could reason for this is that the actual addition of the embed element is not completed by the browser until the JavaScript reaches a pause in processing (ie. not flushed/committed), and that the need to create the alert popup causes the changes to be committed. (does anyone know how to make the browser commit the changes?). My solution to this problem was to add the blank SVG when the page is loaded (if IE) so that it will be available for me to replace when I'm ready.

When the page is loaded, if the browser is IE I call this function to add the blank SVG:

// embedBlankSVG
//
// add an <embed> tag to display a blank static SVG
// this is needed for IE to replace with dynamic content later
SVG.prototype.embedBlankSVG = function(width, height) {
var svgEmbed = document.createElement('embed');
svgEmbed.setAttribute('codebase', 'http://www.adobe.com/svg/viewer/install/');
svgEmbed.setAttribute('classid', 'clsid:78156a80-c6a1-4bbf-8e6a-3cd390eeb4e2');
svgEmbed.setAttribute('pluginspage', 'http://www.adobe.com/svg/viewer/install/');
svgEmbed.setAttribute('src', 'blank.svg');
svgEmbed.setAttribute('width', width);
svgEmbed.setAttribute('height', height);
svgEmbed.setAttribute('type', 'image/svg+xml');
svgEmbed.setAttribute('id', 'embeddedSVG');
this.svgDiv.appendChild(svgEmbed);
};


Then, whenever I want to render a new SVG, I invoke this function:

// render
//
// render the dynamic SVG in the page
SVG.prototype.render = function() {
var str = this.toString();

if (this.isIE) {
// For IE, we use an <embed> tab

// build replacement SVG document
var newSvgElement = document.createElement("svg");
newSvgElement.setAttribute("xmlns", "http://www.w3.org/2000/svg");
newSvgElement.setAttribute('xmlns:xlink', "http://www.w3.org/1999/xlink");
newSvgElement.setAttribute("width", this.width);
newSvgElement.setAttribute("height", this.height);
newSvgElement.innerHTML = str;

// replace blank SVG with new SVG
domtodom(newSvgElement, 'embeddedSVG');
} else {
this.clearPage();

// For all others (mainly Mozilla based), we use an <object> tag
// with the SVG data added inline

var svgObject = document.createElement('object');
svgObject.setAttribute('name', 'svgObj');
svgObject.setAttribute('codebase', 'http://www.adobe.com/svg/viewer/install/');
svgObject.setAttribute('classid', 'clsid:78156a80-c6a1-4bbf-8e6a-3cd390eeb4e2');
svgObject.setAttribute('data', 'data:image/svg+xml,'+ str);
svgObject.setAttribute('width', '850');
svgObject.setAttribute('height', '350');
svgObject.setAttribute('type', 'image/svg+xml');
this.svgDiv.appendChild(svgObject);
}
}


where the toString function produces SVG data from XML data (whether read from a file or returned in an XMLHttpRequest response) and the domtodom function comes from Chris Bayes' domtodom.js

This was tested in Firefox 2.0, IE 7.0, and Opera 9.02 (though the table highlighting isn't currently working in Opera. It says I have a Security Error: attempted to read protected variable).


Help requested


I would like to be able to correspondingly highlight bars in the graph when I mouse over the table rows, but every attempt to add the:

<script type="text/ecmascript">
<![CDATA[
...
]]>
</script>

to the SVG data (as described here) immediately causes Firefox to crash.

Updated 12/07/2006 per comment below


The comment below asked what the clearPage function does. Since I can't format it nicely in the comments section, I decided to update the post with it. It doesn't really do anything too profound which is why I didn't post it originally.

// clear out any old displayed data from the page
SVG.prototype.clearPage = function() {
this.svgDiv.innerHTML = '';
this.tableDiv.innerHTML = '';

if (this.isIE) {
this.embedBlankSVG(this.svgWidth, this.svgHeight);
}
}


Additionally, while I'm updating this posting anyway, I was able to add JavaScript to my SVG through the XSLT (see Help Requested section) by adding it like this in the XSL:

<xsl:element name="script">
<xsl:attribute name="type">
<xsl:value-of select="'text/ecmascript'" />
</xsl:attribute>
<xsl:text>
&lt;![CDATA[
...
]]&gt;
</xsl:text>
</xsl:element>

I haven't gotten it to do everything that I want yet, but at least the browser no longer immediately crashes.

Tuesday, October 24, 2006

Scalable Vector Graphics (SVG) Intro

I ran across an introduction to SVG today entitled, Scalable Vector Graphics (SVG), Creating High-End 2D Graphics Using XML while I was looking for something else, but it gives a nice summary of SVG so I thought I'd share it. It covers the main questions:

  • What is SVG (and what is is not)?
  • How do you use SVG?
  • Why should you use SVG?
It's a few years old (2002), but still a nice article.

Thursday, October 19, 2006

IE7, any real improvement?

Now that IE7 is out, I will be 'upgrading' to it to ensure my web sites support it, but I wonder have much of an upgrade it really is -- especially since they've had 5 years to improve it.

I got an email this morning from eWEEK highlighting Jim Rapoza's glowing review of IE7. One one hand, he acknowledges that IE7 hasn't made any revolutionary step forward in the browser space, but has merely caught up as he says,

While we wouldn't yet call IE 7 one of the best browsers available today, Microsoft has greatly closed the distance between its browser and those of its competitors. Version 7 catches IE up with now-common browser features...



but after summarizing the key user interface and security enhancements, he makes this statement,

On the standards side, IE 7 does a much better job than previous versions of the product at supporting key standards, such as Cascading Style Sheets support and JavaScript. IE 7 still doesn't have perfect standards support, but it is much better than previous versions of the browser.



That statement puzzles me since according to this analysis which summarizes web browser standards support, I see very little improvement in standards support except in CSS 2.1 & 3 Basic Selectors.

To me, it sounds like Microsoft has merely put a new glossy finish on a rotting infrastructure (a whitewashed tomb so to speak) which is really disappointing to me, since after listening to this interview of the IE7 team by the Ajaxians last month, I was more optimistic about standards support.