Weston Ruter

Web application developer in Portland, Oregon

XHTML document.write() Support

Update 6/5: Made modifications to HTMLParser to support more usages, and reverted the changes to it that turned the local variables into member properties since it wasn't necessary.

Update 6/4: Added discussion about supported usages.

Google's AJAX APIs provide some incredible tools which equip web developers to do some amazing things. I've lately been dazzled by the power and accuracy of their AJAX Language API and also of the performance and convenience afforded by their AJAX Libraries API. The only issue I have with their APIs is that the fundamental google.load() function uses document.write() to output the necessary script elements to the DOM, which doesn't even appear to be necessary since google.setOnLoadCallback() executes after the scripts are loaded without using document.write() (but instead appended document with DOM methods). And the reason why using document.write() is bad, of course, is that it is not available when in XHTML.

Likewise, Google's AdSense program provides a great way for web authors to make get some compensation for their hard work. But it too relies on document.write() to output the necessary iframe element to display the advertisement. This has been well noted, and a workaround has been developed which utilizes the an object element. However, there is another solution which enables AdSense to work in XHTML without any HTML workaround, and which allows web authors to use the Google Ajax APIs in XHTML pages: simply define and implement document.write() yourself, as this script does.

When I set out to do this, somehow I completely missed a solution developed by John Resig (one of my biggest JavaScript heroes). My solution, however, has a couple advantages as I see it. First, his solution uses some regular expression hacks to attempt to make the HTML markup well-formed enough for the browser's XML parser, but as he notes, it is not very robust. Secondly, John's solution relies on innerHTML which causes it to completely fail in Safari 2 (although this implementation also fails for an unknown reason). I'm trying a different approach. Instead of using innerHTML, this implementation of document.write() parses the string argument of HTML markup into DOM nodes; if the DOM has not been completely loaded yet, it appends these DOM nodes to the document immediately after the requesting script element; otherwise, it appends the parsed nodes to the end of the body.

I've incorporated John Resig's own HTML Parser (via Erik Arvidsson), but I've made a couple key modifications to make it play nice with document.write(). I turned HTMLParser into a class with member properties in order to save the end state of the parser after all of the buffer has been processed. To this class I added a parse(moreHTML) method which allows additional markup to be passed into the parser for handling so that it can continue parsing from where it had finished from the previous buffer. And by removing the last parseEndTag() cleanup call (for document.write() is anything but clean), it then became possible for multiple document.write() calls to be made with arguments consisting of chopped up HTML fragments like just a start tag or end tag, which is exactly what AdSense does and is a common usage of the method.

Now for some examples, which of course will work when the document is served as either application/xhtml+xml and text/html, as in the case of MSIE. First is the canonical AdSense script elements which output an advertisement:

And to demonstrate handling arbitrary HTML elements with text and comment nodes:

And to illustrate the use of the Google AJAX APIs, the phrase “Hello world!” (loaded from an external script, whose calling script element is itself output by document.write()) is translated into Spanish via Google's AJAX Language API: Loading...

The Language API is loaded via google.load('language', 1) and its completed load-state is detected by google.setOnLoadCallback(). The completed load-state of the sourceText variable in the external script is then detected with this anonymous polling function:

(function(){
    if(window.sourceText){
        //ready to work!
    }
    else
        setTimeout(arguments.callee, 10);
})();

This document.write() implementation is known to work at least in Firefox 2/3, Opera 9.26, and Safari 3. It will work in Internet Explorer, of course, since the document must be served as text/html to be viewed and so it will already have document.write(). For some unknown reason, this currently does not work in Safari 2: the error “TypeError - Undefined value” is raised on the line of HTML where a script element loads xhtml-document-write.js. Any help would be much appreciated. As a workaround in the mean time, simply serve documents as text/html for Safari 2 browsers.

Supported usages: There are three common usages of document.write() in the wild of HTML, and the first two are currently supported:

  1. Outputting a well-formed HTML code fragment:

    document.write('<p>Hello <i>World</i>!</p>');

    This usage is fully supported by this implementation.

  2. Outputting a well-formed HTML code fragment spread out over multiple sequential function calls:

    document.write('<p>');
    document.write('Hello <i>World</i>!');
    document.write('</p>');

    This usage is also supported

  3. script elements with function calls outputting HTML fragments interspersed by arbitrary HTML elements:

    <script type="text/javascript">document.write('<b>');</script>
    Hello <i>World</i>!
    <script type="text/javascript">document.write('</b>');</script>

    This is not supported. Instead of outputting “Hello World!”, this implementation would output one empty b element, followed by just “Hello World!” This usage is more difficult to support although I have an idea of how to do it, but I may not end up implementing it unless there is demand for it.

One restriction, of course, to the use of this implementation is that the entire document must be well-formed XHTML without regard for the markup output by the calls to document.write(); thus you cannot do something like this (via Ian Hixie):

<foo>
 <script type="text/javascript" xmlns="http://www.w3.org/xhtml/1999"/><[!CDATA[
  document.write('<bar>');
 ]]></script>
 </bar>
</foo>

Download (or link to) the script source or the minified version (via Dojo's ShrinkSafe) (except ShrinkSafe currently breaks it).

Please comment with any suggestions or feedback!

This emerged while working for my employer Shepherd Interactive.