.nodeName Case Sensitivity


When working with the DOM .nodeName property there are two hard-and-fast rules that most people abide by:

  1. The node names of HTML elements are always uppercase, even if they’re explicitly created using lowercase characters. <html> will result in a .nodeName === "HTML" (see the HTML 5 draft).
  2. The node names of XML elements are always in the original case, as specified when they’re created. <data> will result in a .nodeName === "data", <DATA> will result in a .nodeName === "DATA".

Knowing these rules can be useful because it allows you to optimize your code. If you know that you’re in an HTML document you can avoid having to upper/lowercase your .nodeName checks and you can just always assume that you’re dealing with a .nodeName that’s uppercase. This results in faster selectors for Internet Explorer and other minor optimizations.

However recently I’ve been running across two cases that’ve been especially problematic and have bucked the trend.

Importing Nodes from XML

The first is for browsers that support the adoptNode/importNode DOM methods. These methods allow you to move (or clone) a node from one DOM document to another. In this way you can move an XML node from an XML document and insert it into an HTML document. Normally this shouldn’t matter much but, as it turns out, the original .nodeName case sensitivity is preserved from the original XML-ness of the node.

Thus if you have a lowercase XML element (<data>) and you use adoptNode or importNode to bring it into your HTML document the result will be .nodeName === "data" — which completely bucks the trend for “all HTML element’s node names are always uppercase.” I consider this to be a bug, considering that the DOM element is now in an HTML document, not in an XML document, and should behave as such.

Unknown HTML 5 Elements

The second bit of weirdness comes from people attempting to use the new elements from HTML 5 in browsers that don’t support it. Most browsers behave perfectly well when using some of the new HTML 5 elements (in that they don’t freak out and support some level of styling). For Internet Explorer you must use the HTML 5 Shim technique – this will give unknown HTML 5 elements the ability to be styled and hold contents (such as a <section> element).

However there is an additional gotcha: When Internet Explorer encounters an element that it doesn’t recognize it leaves the .nodeName in its original case. Thus if you have a <section> element in your HTML page the result will be .nodeName === "section" — which directly contradicts the normal case sensitivity of the .nodeName property in HTML documents.

To try and understand all of this I made a bunch of test cases using a number of doctypes and document styles.

The important part of the test page is quite simple:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4.   <title>Testing nodeName</title>
  5. </head>
  6. <body>
  7.   <div id="test">
  8.     <div></div><DIV></DIV>
  9.     <section></section><SECTION></SECTION>
  10.   </div>
  11. </body>
  12. </html>

and the test cases are as follows:

HTML

Accesses the HTML elements that were originally included the page (should be case insensitive).

  1. runTest("HTML", function(){
  2.   return document.getElementById("test").childNodes;
  3. });

HTML createElement

Creates new DOM elements using the same document as the page in which it was shipped (should be case insensitive).

  1. runTest("HTML createElement", function(){
  2.   return [    
  3.     document.createElement("div"),
  4.     document.createElement("DIV"),
  5.     document.createElement("section"),
  6.     document.createElement("SECTION")
  7.   ];          
  8. });

innerHTML

Attempts to inject the elements using .innerHTML (should be case insensitive).

  1. runTest("innerHTML", function(){
  2.   var test = document.getElementById("test");
  3.   test.innerHTML = "<div></div><DIV></DIV>" +
  4.     "<section></section><SECTION></SECTION>";
  5.   return test.childNodes;
  6. });

For the remaining tests I grab a simple XML document:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <test>
  3.   <div></div><DIV></DIV>
  4.   <section></section><SECTION></SECTION>
  5. </test>

like so:

  1. var xhr = window.XMLHttpRequest ?
  2.     new XMLHttpRequest() :
  3.     new ActiveXObject("Microsoft.XMLHTTP");
  4.  
  5. xhr.open("GET", "test.xml", false);
  6. xhr.send(null);
  7.  
  8. var xml = xhr.responseXML;

XML

Test the elements in the XML document directly (should be case sensitive).

  1. runTest("XML", function(){
  2.   return xml.documentElement.childNodes;
  3. });

XML createElement

Same as the HTML createElement but done using the XML document (should be case sensitive).

  1. runTest("XML createElement", function(){
  2.   return [    
  3.     xml.createElement("div"),
  4.     xml.createElement("DIV"),
  5.     xml.createElement("section"),
  6.     xml.createElement("SECTION")
  7.   ];          
  8. });

HTML via importNode

This clones the nodes from the XML document, using importNode, and places them into the HTML document (should be case sensitive).

  1. runTest("HTML via importNode", function(){
  2.   var test = document.getElementById("test");
  3.   while ( test.firstChild ) {
  4.     test.removeChild( test.firstChild );
  5.   }          
  6.  
  7.   var nodes = xml.documentElement.childNodes, node;
  8.   for ( var i = 0; i < nodes.length; i++ ) {
  9.     node = document.importNode( nodes[i], false );
  10.     test.appendChild( node );
  11.   }
  12.  
  13.   return test.childNodes;
  14. });

HTML via adoptNode

This moves the nodes from the XML document, using adoptNode, and places them into the HTML document (should be case sensitive).

  1. runTest("HTML via adoptNode", function(){
  2.   var test = document.getElementById("test");
  3.   while ( test.firstChild ) {
  4.     test.removeChild( test.firstChild );
  5.   }
  6.  
  7.   var nodes = xml.documentElement.childNodes, node;
  8.   while ( nodes.length ) {
  9.     node = document.adoptNode( nodes[0] );
  10.     test.appendChild( node );
  11.   }
  12.  
  13.   return test.childNodes;
  14. });

The Results

I ran the following tests in IE 6, IE 7, IE 8, Firefox 3.5, Safari 4.0.3, Chrome 3.0.195, and Opera 10.10. Additionally I tested against .tagName in addition to .nodeName and found no discernible difference (you can run your own .tagName tests by appending a ?tagName to any test URL like so.)

HTML 5 Document

Note: The HTML 5, XHTML (served as HTML), and no-doctype pages all behaved identically to each other in every browser – thus I’m just going to not display the XHTML (as HTML) and no-doctype results as there wouldn’t be anything interesting to show.

Firefox, Safari, and Chrome all yielded the same results here: Bringing in elements from an external document maintains the case sensitive nature of the .nodeName property – which is unexpected.

<div> <DIV> <section> <SECTION>
HTML DIV DIV SECTION SECTION
HTML createElement DIV DIV SECTION SECTION
innerHTML DIV DIV SECTION SECTION
XML div DIV section SECTION
XML createElement div DIV section SECTION
HTML via importNode div DIV section SECTION
HTML via adoptNode div DIV section SECTION

Internet Explorer fails in a different manner. To start, Internet Explorer doesn’t support importNode or adoptNode so those particular tests simply don’t run. However we can confirm that the case sensitivity of the unknown HTML 5 element is maintained in HTML, even though it shouldn’t be.

<div><DIV><section><SECTION>
HTMLDIVDIVsectionSECTION
HTML createElementDIVDIVsectionSECTION
innerHTMLDIVDIVsectionSECTION
XMLdivDIVsectionSECTION
XML createElementdivDIVsectionSECTION
HTML via importNodeError: Object doesn’t support this property or method
HTML via adoptNodeError: Object doesn’t support this property or method

Opera ups the ante one further: Since it attempts to simultaneous follow web standards, and implement Internet Explorer’s weird quirks, it both fails the importNode/adoptNode and the HTML 5 unknown element cases.

<div><DIV><section><SECTION>
HTMLDIVDIVsectionSECTION
HTML createElementDIVDIVsectionSECTION
innerHTMLDIVDIVsectionSECTION
XMLdivDIVsectionSECTION
XML createElementdivDIVsectionSECTION
HTML via importNodedivDIVsectionSECTION
HTML via adoptNodedivDIVsectionSECTION

XHTML (served with correct mimetype)

Nearly every browser that supported showing this page (Firefox, Safari, Opera, Chrome) displayed the same, expected, results:

<div> <DIV> <section> <SECTION>
HTML div DIV section SECTION
HTML createElement div DIV section SECTION
innerHTML div DIV section SECTION
XML div DIV section SECTION
XML createElement div DIV section SECTION
HTML via importNode div DIV section SECTION
HTML via adoptNode div DIV section SECTION

An XHTML page served properly is just an XML document – thus the case of elements is sensitive (as to be expected).

… except in Opera. Opera apparently will treat div elements case insensitively, when injected using .innerHTML, even if it’s being served within an XHTML document.

<div> <DIV> <section> <SECTION>
HTML div DIV section SECTION
HTML createElement div DIV section SECTION
innerHTML DIV DIV section SECTION
XML div DIV section SECTION
XML createElement div DIV section SECTION
HTML via importNode div DIV section SECTION
HTML via adoptNode div DIV section SECTION

Update: XHTML as XML Tests

Based upon some suggestions in the comments I’ve run some additional tests. Namely I tested the loading of an XML document that has the correct XHTML namespace attached to it (specifically I used the same XHTML test page that I used for the other tests, just appending a .xml extension instead of .xhtml). The results are rather interesting – and promising, at least. (Note: Internet Explorer continues to fail as it doesn’t have an adoptNode/importNode method.)

Firefox continues to fail the importing of XML nodes, even when they’re coming from an XML document:

<div> <DIV> <section> <SECTION>
HTML DIV DIV SECTION SECTION
HTML createElement DIV DIV SECTION SECTION
innerHTML DIV DIV SECTION SECTION
XML div DIV section SECTION
XML createElement div DIV section SECTION
HTML via importNode div DIV section SECTION
HTML via adoptNode div DIV section SECTION
XML (XHTML) div DIV section SECTION
XHTML via importNode div DIV section SECTION

As does Opera:

<div><DIV><section><SECTION>
HTMLDIVDIVsectionSECTION
HTML createElementDIVDIVsectionSECTION
innerHTMLDIVDIVsectionSECTION
XMLdivDIVsectionSECTION
XML createElementdivDIVsectionSECTION
HTML via importNodedivDIVsectionSECTION
HTML via adoptNodedivDIVsectionSECTION
XML (XHTML)divDIVsectionSECTION
XHTML via importNodedivDIVsectionSECTION

BUT both Safari and Chrome PASS on the importing of XHTML nodes, coming from an XML document:

<div> <DIV> <section> <SECTION>
HTML DIV DIV SECTION SECTION
HTML createElement DIV DIV SECTION SECTION
innerHTML DIV DIV SECTION SECTION
XML div DIV section SECTION
XML createElement div DIV section SECTION
HTML via importNode div DIV section SECTION
HTML via adoptNode div DIV section SECTION
XML (XHTML) div DIV section SECTION
XHTML via importNode DIV DIV SECTION SECTION


This, in particular, is great news. It means that, at least, one browser understands the concept of loading in external (X)HTML into an HTML document and having it continue to work. It’s unfortunate that it doesn’t work in all browsers, though.

Conclusion

What can we learn from all of this? Unfortunately it appears as if we can’t really trust our “trusted” rules about .nodeName case sensitivity for HTML documents. XML documents are completely safe and work as expected. XHTML (served with the correct mimetype) documents are nearly safe, save for the one bizarre Opera bug.

How will this change the code that we write? In short we can no longer trust the case insensitive nature of HTML documents – we need to assume that BOTH HTML and XML documents will be serving their content in a case sensitive nature – especially as more people start to adopt HTML 5 elements in their pages and expect some level of support in older browsers. This means that a number of selectors and DOM methods will take a performance hit as we can no longer take a case insensitive shortcut in our codebases.

There are a few outstanding jQuery tickets that are the result of these issues cropping up and now that I know the reasoning behind why they’re happening I can now strip out all the case-insensitive performance improvements from the codebase – which is really quite unfortunate but at least it’ll behave more consistently. I continue to stand by thesis from my earlier talk about the DOM: The DOM is a mess and every DOM method and property is broken in some way, in some browser.

Posted: November 24th, 2009


If you particularly enjoy my work, I appreciate donations given with Gittip.

31 Comments (Show Comments)



Comments are closed.
Comments are automatically turned off two weeks after the original post. If you have a question concerning the content of this post, please feel free to contact me.


Secrets of the JavaScript Ninja

Secrets of the JS Ninja

Secret techniques of top JavaScript programmers. Published by Manning.

Ukiyo-e Database and Search

Ukiyo-e.org

Japanese woodblock print database and search engine.


John Resig Twitter Updates

@jeresig

Infrequent, short, updates and links.


via Ad Packs