Showing posts with label XSLT. Show all posts
Showing posts with label XSLT. Show all posts

Another XSLT-related bug in IE

I’ve created an RSS feed that points to various web pages that use client-side XSLT (read this article if you’re not familiar with the architecture of my web sites). The feed is accessible through FeedBurner which modifies the feed slightly to track clicks:

  • All the links in the feed are modified to point to FeedBurner URLs
  • The FeedBurner URLs encode the actual click destination (this allows FeedBurner to count the clicks)
  • FeedBurner uses “301 Moved Permanently” status code to redirect the user to the target web page.

When the target web page is an XML document with xml-stylesheet directive, Firefox and Chrome display it correctly, but IE 8 crashes, as it interprets the XSLT stylesheet address relative to the “old” location (FeedBurner), not the redirected location.

I could fix this problem by hard-coding absolute XSLT URL in the XML document, but then some browser might consider that a potential CSS attack, causing further problems. For the moment, IE 8 has proven to be closer to Firefox and its XSLT woes that I thought.

XSLT: Truncate text with ellipsis

A great set of XSLT templates that allows you to truncate XHTML markup (or any XML node) at specified text length. I was impressed ;)

Opinion: Is the Firefox development model really better than Microsoft’s?

Everyone loves bashing Microsoft and preaching the vast superiority of Firefox over IE, particularly its near-perfect implementation of W3C standards. Unfortunately, that might be true as long as you’re doing what the most vocal advocates (the CSS folks) are doing, if you happen to rely on some other “supported” standard, you might find yourself in deep ****.

As my regular readers know, I happened to make a decision to use client-side XSLT a while ago. From the technology standpoint, it was a perfect solution … until Firefox 3 came out. It has so many XSLT-related problems that it’s almost impossible to get the right combination of relevant parameters to have the same XSLT stylesheet working on Firefox, IE, Chrome and the web server (for non-XSLT-capable clients).

OK, one would understand that every major software project has bugs. But it’s hard to understand that so many of the XSLT bugs are untouched after several years. For example: generic ticket describing XSLT result document problems (4 years), HTML-DOM initialization issues (5 years), Firefox crashing when combining XSLT with document.write (6 years), Firebug not working on XSLT-generated pages (1,5 year).

I’ve heard all about limited resources and priorities in my “previous life”, but let’s face the reality: once the motivation (let’s beat IE) wears off and the platform accumulates years of old sins and bad decisions, the development model doesn’t matter. Firefox is becoming no better than IE.

Firefox XSLT errors

When I decided to use server-side XML output and client-side XSLT transformations a while ago, Firefox 2 was the ideal development platform – reliable, well implemented browser with excellent debugging capabilities (Firebug). Since then, the XSLT ignorami contributing code to Firefox have managed to break so many things I simply have to document everything I know to be broken in one place:

On top of everything else, Firebug does not work.

It breaks my heart, but I have to admit that with the rollout of Firefox 3 Internet Explorer 7 does a better job of handling client-side XSLT than Firefox.

Firebug does not work on XSLT-translated pages

If you want to use Firebug with Firefox 3 on pages using client-side XSLT transformation, you’re out of luck. Firebug simply does not work; the only way to a half-working environment is the following:

  • Restart the browser
  • Open an XML page with the xml-stylesheet directive.
  • Open a new tab and load a regular page into it.
  • Switch back to the previous tab. Firebug works within the context of the currently loaded page.

What’s really sad is that this has been reported by one of the Firebug developers almost two years ago … and nobody found it important enough to change the bug status from NEW to anything else.

The inconsistent consistencies are driving me crazy

Here’s a simple question: what’s wrong with this code?

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns:xf="http://www.example.com/common/xslforms">
...
<input type="button" value="X" id="tb_0" onClick="callX(0)" />

Among other things, this is not valid XHTML (as claimed in DOCTYPE) because the onClick attribute should be spelled in all-lowercase (onclick).

Now the trick question: why am I so annoyed by this code? Because it works in IE7, FF2 and FF3 if the server serves HTML to the client and fails only in FF3 if the client performs XSLT transformation using a stylesheet similar to this one:

<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:output method="html" encoding="utf-8"
  doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN" doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"/>

<xsl:template match="/">
  <html xmlns:xf="http://www.plezanje.net/common/xslforms">
…
  <input type="button" value="X" id="tb_0" onClick="callX(0)" />
…
</xsl:template>

The ever-more-crazy implementation of XSLT in Firefox will persuade me to:

  • Stop using client-side XSLT transformations or
  • Stop supporting Firefox and recommend everyone to use IE or Chrome.

How crazy can it get: IE does not set the Referer field if the page was built via client-side XSLT

Some of my web sites rely on client-side XSLT transformations, triggered with the <?xml-stylesheet > processing instruction in the XML document sent by the server. A long time ago I've noticed that some of the utility functions perform differently in IE than in Firefox and finally decided to figure out what's going on. The results are "fascinating": when you click on a link in a page generated with client-side XSLT transformation, IE does not set the Referer: field in the HTTP request. I can only congratulate the coders ... you have an amazing mindset ... not to mention the QA department.

Detect whether your browser has a working XSLT implementation

After I’ve finally managed to persuade IE7, FF and Transform jQuery plugin to work with my XSL documents, I’ve started testing the other two browsers I have: Opera and Chrome. The latest release of Opera might occasionally work. Chrome fails (as expected) as it doesn’t support xsl:import. Welcome to the next round of browser incompatibilities.

I’ve decided to ignore Chrome until the great minds @ Google decide to implement XSLT properly. Visitors using it will have reduced experience … but of course I have to detect whether I should use XSLT or not.

Here’s a small Christmas gift if you have similar issues: a jQuery extension that checks whether the current visitor can use XSLT transformations.

jQuery.xslt = {
  need: function(success,url,xml) {
    var componentCount = 0;
    var xslt;
    var el;
    var debug=1;
    
    function tryTransform() {
      if (typeof(xslt) != 'object') return; // XSL did not parse into XML object
      var html = $.transform({xmlstr: xml, xslobj: xslt, async: false});
      alert("html="+html);
      if (!html) return;
    }

    function gotScript() {

      function transformFail(html,xsl,xml,obj,ex) { 
        if(debug) alert ("$.xslt.need failed:"+ex.message); }
    
      function transfromDone(html,xsl,xml,obj) { 
        $.xslt.has = html.search($.xslt.expectedResult) >= 0;
        if ($.xslt.has && success) success();
      }
      
      el = $("<div>");
      el.transform({xmlstr: xml, xsl: url, 
        success: transfromDone, error: transformFail});
    }
    
    url = url ? url : $.xslt.defaultURL;
    xml = xml ? xml : $.xslt.defaultXML;
    $.getScript($.xslt.transformScript,gotScript);
  },
  has: false,
  
  defaultURL: "/forms/xml/static.xsl",
  defaultXML: "<section />",
  transformScript: "/common/js/jquery.transform.packed.js",
  expectedResult:  /table/i
}

The extension implements a simple function and a property: $.xslt.need() and $.xslt.has. You can pass the URL to the sample XSL document and the sample XML markup to the need function; it also supports a success callback to tell you whether you should install XSL-related action handlers.

The need function loads the jQuery Transform plugin (reducing the load time if the page does not need the XSLT functionality) and tries to transform a sample XSL document. The sample transformation should be as complex as possible: you should use xsl:import, xsl:include or the document() function if you use them in other transformations.

You could change the default parameters in the source code or write a setup function.

IE7, xsl:import and jQuery Transform plugin

After exhausting all other (more convenient from my perspective) approaches, I’ve had to admit that there’s only a single solution that allows you to include/import XSL stylesheets with the jQuery Transform plugin if you want to:

  • Use XSL documents from another directory on the Web server.
  • Use xsl:import or xsl:include in the XSL documents.
  • Have a reliable cross-browser implementation.

The solution, as you might have guessed, is to use the URL pointing to the XSL document in the call to $.transform (the xsl parameter). And just in case you’re wondering why I had to try everything else: I wanted to use the $.transform() call to get the transformed HTML without the delay incurred by setting the async parameter to false.

Xsl:import fails when using XSL object in $.transform

After the initial set of xsl:import related problems I’ve encountered running jQuery Transform plugin in IE7, I’ve tried to pass the XML document as string (using the xmlstr parameter) and XSL as parsed object (using the xslobj parameter) returned by the $.get call. Works perfectly in FF, fails miserably in IE7. The error message indicates that the imports requested with xsl:import don’t work at all. Back to the drawing board …

Import/include problems in jQuery Transform plugin

I started testing the jQuery Transform plugin with a set of pretty convoluted XSLT documents that I use on the target web site, which relies almost exclusively on browser-side XSLT transformations. All server responses are initially encoded as XML and transformed on the server only if the browser does not support XSLT (or if a spider has come to visit us). Not surprisingly, the XSLTs are heavily loaded with xsl:import and xsl:include statements.

Initially, I tried to control as much as possible: read XML and XSLT into strings and perform the transformation when everything has been collected. This approach fails miserably in IE7. The reason is simple: if I pass XSLT source or parsed XSLT object to the $.transform routine, the routine tries to fix xsl:import and xsl:include references using the current web page’s URL as the base reference, which is wrong if the stylesheet has been loaded from another directory.

Conclusion: if you read XSL documents from another directory and pass them as string to the $.transform routine, make sure you use absolute references in import/include statements.

jQuery Transform plugin

Finally I found some time to start working on XSLT transformations in jQuery framework. I may be completely wrong, but I decided to go with the Transform plugin. If anyone has found a better plugin, I would highly appreciate your feedback :).

Poor man's AJAX: Browser-side XSLT transformation in IFRAME

One of the oldest methods to provide AJAX-like functionality in a browser that does not support XmlHttpRequest object is loading the dynamic content into a hidden IFRAME. It’s an unreliable technique that should not be used, but it can still provide a viable workaround in scenarios where you need to perform browser-side XSLT transformation in browsers with lousy JavaScript-based XSLT support (it looks like Chrome is still in this category).

This is a conceptual step-by-step description of the process:

  • Create a hidden IFRAME in the main page.
  • Define a callback function in the main page that will receive the results of the transformation. This callback function will have to insert the transformation results into the main page’s HTML/DOM structure.
  • When you need to download additional content, set the IFRAME.href attribute to the URL of the dynamic content.

You should use a timeout in the main page to capture failed loads of the dynamic content. The timeout code should display an error message and kill the download process in IFRAME by resetting the IFRAME.href attribute.

  • The dynamic content loaded into the IFRAME should have XML MIME type and contain the xml-stylesheet processing instruction. This will cause the browser to perform XSLT transformation on the XML data.
  • The resulting HTML should include JavaScript call of the callback function in the parent frame, for example <body onload="parent.callback(document.body.innerHTML)">

Alternatively, you can use the onload handler in the IFRAME element to call the callback function.

Firefox forgets to create document.body object on Linux

If you use client-side XSLT transformations driven by xml-stylesheet pseudo-instruction in XML documents, you might encounter interesting problems when using Firefox 2 on Linux. When the  HTML page is created with XSLT transformation, Firefox does not create the document.body object, causing JavaScript libraries (for example, jQuery) to break.

Workaround: add an ID to your body tag (for example, <body id="body">) and fix the document.body object after the DOM is ready. To do it in jQuery, use the following code:

$(function() {   if (!document.body) document.body = $('#body').get(0); }

XSLT transformation in ASP: non-standard XML encoding

If you want to generate documents with non-standard encodings with server-side XSLT transformation in IIS/ASP environment, you should:

  • Set the Response.Charset property to the desired character set;
  • Set the Response.Codepage property to the desired code page (or use the <%@ LANGUAGE="VBScript" CODEPAGE="codepage" %> directive in your ASP script).
  • Omit XML declaration from the translated text (using omit-xml-declaration=”yes” attribute in xsl:output element) and prepend the desired XML declaration in front of the translated text.

For example, the following program generates XML (or XHTML) document encoded in windows-1250 character set (codepage: 1250) …

<%
Const DOMClass = "MSXML2.DOMDocument"
Set XSLT = Server.CreateObject(DOMClass)
Set XDoc = Server.CreateObject(DOMClass)
XDoc.loadXML("<root greek='&#946;' ee='č' />")
XSLT.load(Server.MapPath("SampleXSLT.xsl"))
Response.Clear
Response.Charset = "windows-1250"
Response.Codepage = 1250
Response.ContentType = "text/xml"
Response.Write "<?xml version='1.0' encoding='windows-1250'?>"
Response.Write XDoc.transformNode(XSLT)
%>

… when using the following XSL transformation:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" omit-xml-declaration="yes" />
… rest deleted …

XSLT transformation in ASP: XML with MSXML6

Similar to the XML-to-HTML transformation with MSXML6, the results of the transformNode function do not include the encoding information; the XML text produced by the sample program with the XSL transformation described in the previous post is a perfect XML document:

<?xml version="1.0"?>
<output>
  Greek letter: β 
  EE: č
</output>

An XML document without the encoding attribute in the xml declaration is assumed to be encoded in UTF-8. The results of the transformNode function in MSXML6 are thus absolutely correct if you use UTF-8 output encoding.

XSLT transformation in ASP: XML with MSXML3

You might consider server-side XML-to-XML transformations rare, but you have to use <xsl:output method=”xml”> if you want to generate valid XHTML code from your XML data. To shorten the printouts, we’ll use a simple (non-XHTML) XSL transformation to generate the test results:

<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    
<xsl:output method="xml" encoding="utf-8" />

<xsl:template match="root">
  <output>
    Greek letter: <xsl:value-of select="@greek" /> 
    EE: <xsl:value-of select="@ee" />
  </output>
</xsl:template> 

</xsl:stylesheet>

As with HTML, MSXML3 interferes with the output, inserting improper UTF-16 encoding directive, resulting in the following XML text:

<?xml version="1.0" encoding="UTF-16"?>
<output>
  Greek letter: β 
  EE: č
</output>

You could bypass this bug by setting the omit-xml-declaration attribute of the xsl:output element to yes

<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    
<xsl:output method="xml" encoding="utf-8" omit-xml-declaration="yes" />

… rest deleted …

… resulting in the following transformation output:

<output>
  Greek letter: β 
  EE: č
</output>

However, if you want to retain XML declaration in the XML document, you have to replace UTF-16 in the output string with UTF-8, like we did in the HTML transformation case. The following modified test program produces perfect XML document when used with the original XSLT transformation (without the omit-xml-declaration attribute):

<%
Const DOMClass = "MSXML2.DOMDocument"

Set XSLT = Server.CreateObject(DOMClass)
Set XDoc = Server.CreateObject(DOMClass)

XDoc.loadXML("<root greek='β' ee='č' />")
XSLT.load(Server.MapPath("SampleXSLT.xsl"))

Response.Clear
Response.Charset = "utf-8"
Response.Codepage = 65001
Response.Write Replace(XDoc.transformNode(XSLT), _
  "encoding=""UTF-16""","encoding=""utf-8""")
%>

XSLT transformation in ASP: HTML with MSXML6

If your IIS platform includes MSXML6, you should use MSXML6 to transform XML into HTML instead of MSXML3 (see the Using the right version of MSXML article for proper fallback process). MSXML6 still generates the META tag (the MSXML3-related post describes problems caused by the META tag), but does not include the charset parameter in it, resulting in perfect HTML output. The sample ASP program using the simple XSLT transformation from the “XSLT transformation in ASP: HTML with MSXML3” post produces the following output when using MSXML6 (MSXML2.DomDocument.6.0 ProgID):

<html>
<head>
<META http-equiv="Content-Type" content="text/html">
<title>Sample XSLT server-side transformation</title>
</head>
<body><node type="test">Greek letter: β EE: č</node></body>
</html>

If you want to use complex XSLT transformations with MSXML6, you might have to set additional second-level DOM properties, for example AllowDocumentFunction. You might also want to set ValidateOnParse to false if the validity of your XML document is not a major concern.

XSLT transformation in ASP: HTML with MSXML3

Most commonly, you’ll use server-side XSLT transformation in ASP to transform XML data into HTML using MSXML3 (available on almost all IIS platforms). The following XSLT stylesheet is used in the HTML test:

<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    
<xsl:output method="html" encoding="utf-8" />

<xsl:template match="root">
  <html>
    <head>
      <title>Sample XSLT server-side transformation</title>
    </head>
    <body>
      <node type="test">
        Greek letter: <xsl:value-of select="@greek" /> 
        EE: <xsl:value-of select="@ee" />
      </node>
    </body>
  </html>
</xsl:template> 

</xsl:stylesheet>

With MSXML3, the transformNode function inserts a META directive in the output stream, resulting in the following transformation result:

<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-16">
<title>Sample XSLT server-side transformation</title>
</head>
<body><node type="test">
Greek letter: β 
EE: č
</node>
</body>
</html>

On the other hand, the HTTP headers generated by the test ASP script claim the content is UTF-8 encoded (and the raw data dump performed with Fiddler confirms that). The response headers were taken from LiveHTTPHeaders Firefox extension:

HTTP/1.x 200 OK
Server: Microsoft-IIS/5.1
Date: Mon, 15 Sep 2008 11:20:56 GMT
X-Powered-By: ASP.NET
Content-Length: 222
Content-Type: text/html; Charset=utf-8
Cache-Control: private

Most browsers (including IE and Firefox) take the Content-type from HTTP headers, using the META directive as a fallback. They thus render the page as intended. Some browsers (including the TextView tab of Fiddler) prefer the META directive and produce garbage.

The solution

To match the META header with the HTTP headers, replace the charset=UTF-16 string in the text returned by the transformNode function with charset=UTF-8. The modified sample ASP script is included below:

<%
Const DOMClass = "MSXML2.DOMDocument"

Set XSLT = Server.CreateObject(DOMClass)
Set XDoc = Server.CreateObject(DOMClass)

XDoc.loadXML("<root greek='β' ee='č' />")
XSLT.load(Server.MapPath("SampleXSLT.xsl"))

Response.Clear
Response.Charset = "utf-8"
Response.Codepage = 65001
Response.Write Replace(XDoc.transformNode(XSLT),"charset=UTF-16","charset=UTF-8")
%>

XSLT transformation in ASP: The testbed

As I’ve mentioned in the previous post, MSXML functions called from ASP believe they work in UTF-16 environment. The transformNode function might insert this information in the output string based on several parameters, one of them being the version of the MSXML ActiveX control. To test behavior of various versions of MSXML, we’ll use the following test program:

<%
Const DOMClass = "MSXML2.DOMDocument"

Set XSLT = Server.CreateObject(DOMClass)
Set XDoc = Server.CreateObject(DOMClass)

XDoc.loadXML("<root greek='β' ee='č' />")
XSLT.load(Server.MapPath("SampleXSLT.xsl"))

Response.Clear
Response.Charset = "utf-8"
Response.Codepage = 65001
Response.Write XDoc.transformNode(XSLT)
%>

And a stylesheet similar to this one:

<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    
<xsl:output method="xml" encoding="utf-8" />

<xsl:template match="root">
  <output>
    <node type="test">
      Greek letter: <xsl:value-of select="@greek" /> 
      EE: <xsl:value-of select="@ee" />
    </node>
  </output>
</xsl:template> 

</xsl:stylesheet>

We’ll test two MSXML versions: MSXML3 (default with Windows XP) and MSXML6 (default with Vista, also available on Windows XP). MSXML3 should be available on almost all web servers, you might not get MSXML6 everywhere (my hosting provider did not offer it when it mattered most to me).

The test results will be published in the next few posts.

Further reading: