[html5lib] api updates: localName

R=sigmund@google.com

Review URL: https://codereview.chromium.org//178303009

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/third_party/html5lib@33004 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/lib/dom.dart b/lib/dom.dart
index 12d6647..b81c1ff 100644
--- a/lib/dom.dart
+++ b/lib/dom.dart
@@ -75,9 +75,13 @@
   static const int PROCESSING_INSTRUCTION_NODE = 7;
   static const int TEXT_NODE = 3;
 
-  // TODO(jmesserly): this should be on Element
-  /// The tag name associated with the node.
-  final String tagName;
+  /// Note: For now we use it to implement the deprecated tagName property.
+  final String _tagName;
+
+  /// *Deprecated* use [Element.localName] instead.
+  /// Note: after removal, this will be replaced by a correct version that
+  /// returns uppercase [as specified](http://dom.spec.whatwg.org/#dom-element-tagname).
+  @deprecated String get tagName => _tagName;
 
   /// The parent of the current node (or null for the document node).
   Node parent;
@@ -104,7 +108,10 @@
   LinkedHashMap<dynamic, FileSpan> _attributeSpans;
   LinkedHashMap<dynamic, FileSpan> _attributeValueSpans;
 
-  Node(this.tagName) {
+  /// *Deprecated* use [new Element.tag] instead.
+  @deprecated Node(String tagName) : this._(tagName);
+
+  Node._([this._tagName]) {
     nodes._parent = this;
   }
 
@@ -138,7 +145,8 @@
   /// name and attributes but with no parent or child nodes.
   Node clone();
 
-  String get namespace => null;
+  /// *Deprecated* use [Element.namespaceUri] instead.
+  @deprecated String get namespace => null;
 
   int get nodeType;
 
@@ -148,23 +156,31 @@
   /// *Deprecated* use [nodeType].
   @deprecated int get $dom_nodeType => nodeType;
 
-  String get outerHtml {
+  /// *Deprecated* use [Element.outerHtml]
+  @deprecated String get outerHtml => _outerHtml;
+
+  /// *Deprecated* use [Element.innerHtml]
+  @deprecated String get innerHtml => _innerHtml;
+  @deprecated set innerHtml(String value) { _innerHtml = value; }
+
+  // http://domparsing.spec.whatwg.org/#extensions-to-the-element-interface
+  String get _outerHtml {
     var str = new StringBuffer();
     _addOuterHtml(str);
     return str.toString();
   }
 
-  String get innerHtml {
+  String get _innerHtml {
     var str = new StringBuffer();
     _addInnerHtml(str);
     return str.toString();
   }
 
-  set innerHtml(String value) {
+  set _innerHtml(String value) {
     nodes.clear();
     // TODO(jmesserly): should be able to get the same effect by adding the
     // fragment directly.
-    nodes.addAll(parseFragment(value, container: tagName).nodes);
+    nodes.addAll(parseFragment(value, container: _tagName).nodes);
   }
 
   // Implemented per: http://dom.spec.whatwg.org/#dom-node-textcontent
@@ -181,8 +197,6 @@
     for (Node child in nodes) child._addOuterHtml(str);
   }
 
-  String toString() => tagName;
-
   Node remove() {
     // TODO(jmesserly): is parent == null an error?
     if (parent != null) {
@@ -216,10 +230,9 @@
   /// Return true if the node has children or text.
   bool hasContent() => nodes.length > 0;
 
-  Pair<String, String> get nameTuple {
-    var ns = namespace != null ? namespace : Namespaces.html;
-    return new Pair(ns, tagName);
-  }
+  /// *Deprecated* construct a pair using the namespaceUri and the name.
+  @deprecated Pair<String, String> get nameTuple =>
+      this is Element ? getElementNameTuple(this) : null;
 
   /// Move all the children of the current node to [newParent].
   /// This is needed so that trees that don't store text as nodes move the
@@ -308,7 +321,7 @@
   Element _queryType(String tag) {
     for (var node in nodes) {
       if (node is! Element) continue;
-      if (node.tagName == tag) return node;
+      if (node.localName == tag) return node;
       var result = node._queryType(tag);
       if (result != null) return result;
     }
@@ -318,7 +331,7 @@
   void _queryAllType(String tag, List<Element> results) {
     for (var node in nodes) {
       if (node is! Element) continue;
-      if (node.tagName == tag) results.add(node);
+      if (node.localName == tag) results.add(node);
       node._queryAllType(tag, results);
     }
   }
@@ -353,7 +366,7 @@
 }
 
 class Document extends Node {
-  Document() : super(null);
+  Document() : super._();
   factory Document.html(String html) => parse(html);
 
   int get nodeType => Node.DOCUMENT_NODE;
@@ -363,6 +376,14 @@
   Element get head => documentElement.querySelector('head');
   Element get body => documentElement.querySelector('body');
 
+  /// Returns a fragment of HTML or XML that represents the element and its
+  /// contents.
+  // TODO(jmesserly): this API is not specified in:
+  // <http://domparsing.spec.whatwg.org/> nor is it in dart:html, instead
+  // only Element has outerHtml. However it is quite useful. Should we move it
+  // to dom_parsing, where we keep other custom APIs?
+  String get outerHtml => _outerHtml;
+
   String toString() => "#document";
 
   void _addOuterHtml(StringBuffer str) => _addInnerHtml(str);
@@ -385,10 +406,13 @@
 }
 
 class DocumentType extends Node {
+  final String name;
   final String publicId;
   final String systemId;
 
-  DocumentType(String name, this.publicId, this.systemId) : super(name);
+  DocumentType(String name, this.publicId, this.systemId)
+      // Note: once Node.tagName is removed, don't pass "name" to super
+      : name = name, super._(name);
 
   int get nodeType => Node.DOCUMENT_TYPE_NODE;
 
@@ -398,9 +422,9 @@
       // it seems useful, and the parser can handle it, so for now keeping it.
       var pid = publicId != null ? publicId : '';
       var sid = systemId != null ? systemId : '';
-      return '<!DOCTYPE $tagName "$pid" "$sid">';
+      return '<!DOCTYPE $name "$pid" "$sid">';
     } else {
-      return '<!DOCTYPE $tagName>';
+      return '<!DOCTYPE $name>';
     }
   }
 
@@ -409,13 +433,13 @@
     str.write(toString());
   }
 
-  DocumentType clone() => new DocumentType(tagName, publicId, systemId);
+  DocumentType clone() => new DocumentType(name, publicId, systemId);
 }
 
 class Text extends Node {
   String data;
 
-  Text(this.data) : super(null);
+  Text(this.data) : super._();
 
   /// *Deprecated* use [data].
   @deprecated String get value => data;
@@ -425,15 +449,7 @@
 
   String toString() => '"$data"';
 
-  void _addOuterHtml(StringBuffer str) {
-    // Don't escape text for certain elements, notably <script>.
-    if (rcdataElements.contains(parent.tagName) ||
-        parent.tagName == 'plaintext') {
-      str.write(data);
-    } else {
-      str.write(htmlSerializeEscape(data));
-    }
-  }
+  void _addOuterHtml(StringBuffer str) => writeTextNodeAsHtml(str, this);
 
   Text clone() => new Text(data);
 
@@ -442,12 +458,19 @@
 }
 
 class Element extends Node {
-  final String namespace;
+  final String namespaceUri;
 
-  // TODO(jmesserly): deprecate in favor of Element.tag? Or rename?
-  Element(String name, [this.namespace]) : super(name);
+  @deprecated String get namespace => namespaceUri;
 
-  Element.tag(String name) : namespace = null, super(name);
+  /// The [local name](http://dom.spec.whatwg.org/#concept-element-local-name)
+  /// of this element.
+  String get localName => _tagName;
+
+  // TODO(jmesserly): deprecate in favor of [Document.createElementNS].
+  // However we need every element to have a Document before this can work.
+  Element(String name, [this.namespaceUri]) : super._(name);
+
+  Element.tag(String name) : namespaceUri = null, super._(name);
 
   static final _START_TAG_REGEXP = new RegExp('<(\\w+)');
 
@@ -506,25 +529,37 @@
   int get nodeType => Node.ELEMENT_NODE;
 
   String toString() {
-    if (namespace == null) return "<$tagName>";
-    return "<${Namespaces.getPrefix(namespace)} $tagName>";
+    if (namespaceUri == null) return "<$localName>";
+    return "<${Namespaces.getPrefix(namespaceUri)} $localName>";
   }
 
   String get text => _getText(this);
   set text(String value) => _setText(this, value);
 
+  /// Returns a fragment of HTML or XML that represents the element and its
+  /// contents.
+  String get outerHtml => _outerHtml;
+
+  /// Returns a fragment of HTML or XML that represents the element's contents.
+  /// Can be set, to replace the contents of the element with nodes parsed from
+  /// the given string.
+  String get innerHtml => _innerHtml;
+  // TODO(jmesserly): deprecate in favor of:
+  // <https://api.dartlang.org/apidocs/channels/stable/#dart-dom-html.Element@id_setInnerHtml>
+  set innerHtml(String value) { _innerHtml = value; }
+
   void _addOuterHtml(StringBuffer str) {
     // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#serializing-html-fragments
     // Element is the most complicated one.
-    if (namespace == null ||
-        namespace == Namespaces.html ||
-        namespace == Namespaces.mathml ||
-        namespace == Namespaces.svg) {
-      str.write('<$tagName');
+    if (namespaceUri == null ||
+        namespaceUri == Namespaces.html ||
+        namespaceUri == Namespaces.mathml ||
+        namespaceUri == Namespaces.svg) {
+      str.write('<$localName');
     } else {
       // TODO(jmesserly): the spec doesn't define "qualified name".
       // I'm not sure if this is correct, but it should parse reasonably.
-      str.write('<${Namespaces.getPrefix(namespace)}:$tagName');
+      str.write('<${Namespaces.getPrefix(namespaceUri)}:$localName');
     }
 
     if (attributes.length > 0) {
@@ -538,7 +573,8 @@
     str.write('>');
 
     if (nodes.length > 0) {
-      if (tagName == 'pre' || tagName == 'textarea' || tagName == 'listing') {
+      if (localName == 'pre' || localName == 'textarea' ||
+          localName == 'listing') {
         final first = nodes[0];
         if (first is Text && first.data.startsWith('\n')) {
           // These nodes will remove a leading \n at parse time, so if we still
@@ -552,10 +588,10 @@
 
     // void elements must not have an end tag
     // http://dev.w3.org/html5/markup/syntax.html#void-elements
-    if (!isVoidElement(tagName)) str.write('</$tagName>');
+    if (!isVoidElement(localName)) str.write('</$localName>');
   }
 
-  Element clone() => new Element(tagName, namespace)
+  Element clone() => new Element(localName, namespaceUri)
       ..attributes = new LinkedHashMap.from(attributes);
 
   String get id {
@@ -575,7 +611,7 @@
 class Comment extends Node {
   String data;
 
-  Comment(this.data) : super(null);
+  Comment(this.data) : super._();
 
   int get nodeType => Node.COMMENT_NODE;
 
diff --git a/lib/dom_parsing.dart b/lib/dom_parsing.dart
index fa13261..aed9fff 100644
--- a/lib/dom_parsing.dart
+++ b/lib/dom_parsing.dart
@@ -3,6 +3,7 @@
 library dom_parsing;
 
 import 'dom.dart';
+import 'src/constants.dart' show rcdataElements;
 
 /// A simple tree visitor for the DOM nodes.
 class TreeVisitor {
@@ -67,17 +68,17 @@
   }
 
   visitDocumentType(DocumentType node) {
-    _str.write('<code class="markup doctype">&lt;!DOCTYPE ${node.tagName}>'
+    _str.write('<code class="markup doctype">&lt;!DOCTYPE ${node.name}>'
         '</code>');
   }
 
   visitText(Text node) {
-    // TODO(jmesserly): would be nice to use _addOuterHtml directly.
-    _str.write(node.outerHtml);
+    writeTextNodeAsHtml(_str, node);
   }
 
   visitElement(Element node) {
-    _str.write('&lt;<code class="markup element-name">${node.tagName}</code>');
+    final tag = node.localName;
+    _str.write('&lt;<code class="markup element-name">$tag</code>');
     if (node.attributes.length > 0) {
       node.attributes.forEach((key, v) {
         v = htmlSerializeEscape(v, attributeMode: true);
@@ -88,12 +89,12 @@
     if (node.nodes.length > 0) {
       _str.write(">");
       visitChildren(node);
-    } else if (isVoidElement(node.tagName)) {
+    } else if (isVoidElement(tag)) {
       _str.write(">");
       return;
     }
     _str.write(
-        '&lt;/<code class="markup element-name">${node.tagName}</code>>');
+        '&lt;/<code class="markup element-name">$tag</code>>');
   }
 
   visitComment(Comment node) {
@@ -160,3 +161,18 @@
   }
   return false;
 }
+
+/// Serialize text node according to:
+/// <http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#html-fragment-serialization-algorithm>
+void writeTextNodeAsHtml(StringBuffer str, Text node) {
+  // Don't escape text for certain elements, notably <script>.
+  final parent = node.parent;
+  if (parent is Element) {
+    var tag = parent.localName;
+    if (rcdataElements.contains(tag) || tag == 'plaintext') {
+      str.write(node.data);
+      return;
+    }
+  }
+  str.write(htmlSerializeEscape(node.data));
+}
diff --git a/lib/parser.dart b/lib/parser.dart
index c388019..2d04f8e 100644
--- a/lib/parser.dart
+++ b/lib/parser.dart
@@ -249,28 +249,28 @@
     framesetOK = true;
   }
 
-  bool isHTMLIntegrationPoint(Node element) {
-    if (element.tagName == "annotation-xml" &&
-        element.namespace == Namespaces.mathml) {
+  bool isHTMLIntegrationPoint(Element element) {
+    if (element.localName == "annotation-xml" &&
+        element.namespaceUri == Namespaces.mathml) {
       var enc = element.attributes["encoding"];
       if (enc != null) enc = asciiUpper2Lower(enc);
       return enc == "text/html" || enc == "application/xhtml+xml";
     } else {
       return htmlIntegrationPointElements.contains(
-          new Pair(element.namespace, element.tagName));
+          new Pair(element.namespaceUri, element.localName));
     }
   }
 
-  bool isMathMLTextIntegrationPoint(Node element) {
+  bool isMathMLTextIntegrationPoint(Element element) {
     return mathmlTextIntegrationPointElements.contains(
-        new Pair(element.namespace, element.tagName));
+        new Pair(element.namespaceUri, element.localName));
   }
 
   bool inForeignContent(Token token, int type) {
     if (tree.openElements.length == 0) return false;
 
     var node = tree.openElements.last;
-    if (node.namespace == tree.defaultNamespace) return false;
+    if (node.namespaceUri == tree.defaultNamespace) return false;
 
     if (isMathMLTextIntegrationPoint(node)) {
       if (type == TokenKind.startTag &&
@@ -283,7 +283,7 @@
       }
     }
 
-    if (node.tagName == "annotation-xml" && type == TokenKind.startTag &&
+    if (node.localName == "annotation-xml" && type == TokenKind.startTag &&
         (token as StartTagToken).name == "svg") {
       return false;
     }
@@ -491,8 +491,8 @@
   void resetInsertionMode() {
     // The name of this method is mostly historical. (It's also used in the
     // specification.)
-    for (Node node in tree.openElements.reversed) {
-      var nodeName = node.tagName;
+    for (var node in tree.openElements.reversed) {
+      var nodeName = node.localName;
       bool last = node == tree.openElements[0];
       if (last) {
         assert(innerHTMLMode);
@@ -505,7 +505,7 @@
           assert(innerHTMLMode);
           break;
       }
-      if (!last && node.namespace != tree.defaultNamespace) {
+      if (!last && node.namespaceUri != tree.defaultNamespace) {
         continue;
       }
       switch (nodeName) {
@@ -616,7 +616,7 @@
   /// Helper method for popping openElements.
   void popOpenElementsUntil(String name) {
     var node = tree.openElements.removeLast();
-    while (node.tagName != name) {
+    while (node.localName != name) {
       node = tree.openElements.removeLast();
     }
   }
@@ -971,7 +971,7 @@
 
   void endTagHead(EndTagToken token) {
     var node = parser.tree.openElements.removeLast();
-    assert(node.tagName == "head");
+    assert(node.localName == "head");
     parser.phase = parser._afterHeadPhase;
   }
 
@@ -1049,8 +1049,8 @@
       {"name": token.name});
     tree.openElements.add(tree.headPointer);
     parser._inHeadPhase.processStartTag(token);
-    for (Node node in tree.openElements.reversed) {
-      if (node.tagName == "head") {
+    for (var node in tree.openElements.reversed) {
+      if (node.localName == "head") {
         tree.openElements.remove(node);
         break;
       }
@@ -1196,8 +1196,9 @@
     }
   }
 
-  bool isMatchingFormattingElement(Node node1, Node node2) {
-    if (node1.tagName != node2.tagName || node1.namespace != node2.namespace) {
+  bool isMatchingFormattingElement(Element node1, Element node2) {
+    if (node1.localName != node2.localName ||
+        node1.namespaceUri != node2.namespaceUri) {
       return false;
     } else if (node1.attributes.length != node2.attributes.length) {
       return false;
@@ -1234,8 +1235,8 @@
 
   // the real deal
   bool processEOF() {
-    for (Node node in tree.openElements.reversed) {
-      switch (node.tagName) {
+    for (var node in tree.openElements.reversed) {
+      switch (node.localName) {
         case "dd": case "dt": case "li": case "p": case "tbody": case "td":
         case "tfoot": case "th": case "thead": case "tr": case "body":
         case "html":
@@ -1255,7 +1256,7 @@
     dropNewline = false;
     if (data.startsWith("\n")) {
       var lastOpen = tree.openElements.last;
-      if (const ["pre", "listing", "textarea"].contains(lastOpen.tagName)
+      if (const ["pre", "listing", "textarea"].contains(lastOpen.localName)
           && !lastOpen.hasContent()) {
         data = data.substring(1);
       }
@@ -1296,7 +1297,7 @@
   void startTagBody(StartTagToken token) {
     parser.parseError(token.span, "unexpected-start-tag", {"name": "body"});
     if (tree.openElements.length == 1
-        || tree.openElements[1].tagName != "body") {
+        || tree.openElements[1].localName != "body") {
       assert(parser.innerHTMLMode);
     } else {
       parser.framesetOK = false;
@@ -1309,13 +1310,13 @@
   void startTagFrameset(StartTagToken token) {
     parser.parseError(token.span, "unexpected-start-tag", {"name": "frameset"});
     if ((tree.openElements.length == 1 ||
-        tree.openElements[1].tagName != "body")) {
+        tree.openElements[1].localName != "body")) {
       assert(parser.innerHTMLMode);
     } else if (parser.framesetOK) {
       if (tree.openElements[1].parent != null) {
         tree.openElements[1].parent.nodes.remove(tree.openElements[1]);
       }
-      while (tree.openElements.last.tagName != "html") {
+      while (tree.openElements.last.localName != "html") {
         tree.openElements.removeLast();
       }
       tree.insertElement(token);
@@ -1358,13 +1359,13 @@
                                 "dt": const ["dt", "dd"],
                                 "dd": const ["dt", "dd"]};
     var stopNames = stopNamesMap[token.name];
-    for (Node node in tree.openElements.reversed) {
-      if (stopNames.contains(node.tagName)) {
-        parser.phase.processEndTag(new EndTagToken(node.tagName));
+    for (var node in tree.openElements.reversed) {
+      if (stopNames.contains(node.localName)) {
+        parser.phase.processEndTag(new EndTagToken(node.localName));
         break;
       }
-      if (specialElements.contains(node.nameTuple) &&
-          !const ["address", "div", "p"].contains(node.tagName)) {
+      if (specialElements.contains(getElementNameTuple(node)) &&
+          !const ["address", "div", "p"].contains(node.localName)) {
         break;
       }
     }
@@ -1388,7 +1389,7 @@
     if (tree.elementInScope("p", variant: "button")) {
       endTagP(new EndTagToken("p"));
     }
-    if (headingElements.contains(tree.openElements.last.tagName)) {
+    if (headingElements.contains(tree.openElements.last.localName)) {
       parser.parseError(token.span, "unexpected-start-tag",
           {"name": token.name});
       tree.openElements.removeLast();
@@ -1556,7 +1557,7 @@
   }
 
   void startTagOpt(StartTagToken token) {
-    if (tree.openElements.last.tagName == "option") {
+    if (tree.openElements.last.localName == "option") {
       parser.phase.processEndTag(new EndTagToken("option"));
     }
     tree.reconstructActiveFormattingElements();
@@ -1584,7 +1585,7 @@
     if (tree.elementInScope("ruby")) {
       tree.generateImpliedEndTags();
       var last = tree.openElements.last;
-      if (last.tagName != "ruby") {
+      if (last.localName != "ruby") {
         parser.parseError(last.sourceSpan, 'undefined-error');
       }
     }
@@ -1642,7 +1643,7 @@
       endTagP(new EndTagToken("p"));
     } else {
       tree.generateImpliedEndTags("p");
-      if (tree.openElements.last.tagName != "p") {
+      if (tree.openElements.last.localName != "p") {
         parser.parseError(token.span, "unexpected-end-tag", {"name": "p"});
       }
       popOpenElementsUntil("p");
@@ -1653,9 +1654,9 @@
     if (!tree.elementInScope("body")) {
       parser.parseError(token.span, 'undefined-error');
       return;
-    } else if (tree.openElements.last.tagName != "body") {
-      for (Node node in slice(tree.openElements, 2)) {
-        switch (node.tagName) {
+    } else if (tree.openElements.last.localName != "body") {
+      for (Element node in slice(tree.openElements, 2)) {
+        switch (node.localName) {
           case "dd": case "dt": case "li": case "optgroup": case "option":
           case "p": case "rp": case "rt": case "tbody": case "td": case "tfoot":
           case "th": case "thead": case "tr": case "body": case "html":
@@ -1663,7 +1664,7 @@
         }
         // Not sure this is the correct name for the parse error
         parser.parseError(token.span, "expected-one-end-tag-but-got-another",
-            {"gotName": "body", "expectedName": node.tagName});
+            {"gotName": "body", "expectedName": node.localName});
         break;
       }
     }
@@ -1688,7 +1689,7 @@
     if (inScope) {
       tree.generateImpliedEndTags();
     }
-    if (tree.openElements.last.tagName != token.name) {
+    if (tree.openElements.last.localName != token.name) {
       parser.parseError(token.span, "end-tag-too-early", {"name": token.name});
     }
     if (inScope) {
@@ -1721,7 +1722,7 @@
       parser.parseError(token.span, "unexpected-end-tag", {"name": token.name});
     } else {
       tree.generateImpliedEndTags(token.name);
-      if (tree.openElements.last.tagName != token.name) {
+      if (tree.openElements.last.localName != token.name) {
         parser.parseError(token.span, "end-tag-too-early", {"name": token.name});
       }
       popOpenElementsUntil(token.name);
@@ -1735,15 +1736,15 @@
         break;
       }
     }
-    if (tree.openElements.last.tagName != token.name) {
+    if (tree.openElements.last.localName != token.name) {
       parser.parseError(token.span, "end-tag-too-early", {"name": token.name});
     }
 
     for (var item in headingElements) {
       if (tree.elementInScope(item)) {
-        item = tree.openElements.removeLast();
-        while (!headingElements.contains(item.tagName)) {
-          item = tree.openElements.removeLast();
+        var node = tree.openElements.removeLast();
+        while (!headingElements.contains(node.localName)) {
+          node = tree.openElements.removeLast();
         }
         break;
       }
@@ -1766,7 +1767,7 @@
           token.name);
       if (formattingElement == null ||
           (tree.openElements.contains(formattingElement) &&
-           !tree.elementInScope(formattingElement.tagName))) {
+           !tree.elementInScope(formattingElement.localName))) {
         parser.parseError(token.span, "adoption-agency-1.1",
             {"name": token.name});
         return;
@@ -1789,7 +1790,7 @@
       var afeIndex = tree.openElements.indexOf(formattingElement);
       Node furthestBlock = null;
       for (Node element in slice(tree.openElements, afeIndex)) {
-        if (specialElements.contains(element.nameTuple)) {
+        if (specialElements.contains(getElementNameTuple(element))) {
           furthestBlock = element;
           break;
         }
@@ -1866,7 +1867,7 @@
       }
 
       if (const ["table", "tbody", "tfoot", "thead", "tr"].contains(
-          commonAncestor.tagName)) {
+          commonAncestor.localName)) {
         var nodePos = tree.getTableMisnestedNodePosition();
         nodePos[0].insertBefore(lastNode, nodePos[1]);
       } else {
@@ -1898,7 +1899,7 @@
     if (tree.elementInScope(token.name)) {
       tree.generateImpliedEndTags();
     }
-    if (tree.openElements.last.tagName != token.name) {
+    if (tree.openElements.last.localName != token.name) {
       parser.parseError(token.span, "end-tag-too-early", {"name": token.name});
     }
     if (tree.elementInScope(token.name)) {
@@ -1916,17 +1917,17 @@
   }
 
   void endTagOther(EndTagToken token) {
-    for (Node node in tree.openElements.reversed) {
-      if (node.tagName == token.name) {
+    for (var node in tree.openElements.reversed) {
+      if (node.localName == token.name) {
         tree.generateImpliedEndTags(token.name);
-        if (tree.openElements.last.tagName != token.name) {
+        if (tree.openElements.last.localName != token.name) {
           parser.parseError(token.span, "unexpected-end-tag",
               {"name": token.name});
         }
         while (tree.openElements.removeLast() != node);
         break;
       } else {
-        if (specialElements.contains(node.nameTuple)) {
+        if (specialElements.contains(getElementNameTuple(node))) {
           parser.parseError(token.span, "unexpected-end-tag",
               {"name": token.name});
           break;
@@ -1956,7 +1957,7 @@
   bool processEOF() {
     var last = tree.openElements.last;
     parser.parseError(last.sourceSpan, "expected-named-closing-tag-but-got-eof",
-        {'name': last.tagName});
+        {'name': last.localName});
     tree.openElements.removeLast();
     parser.phase = parser.originalPhase;
     return true;
@@ -1964,7 +1965,7 @@
 
   void endTagScript(EndTagToken token) {
     var node = tree.openElements.removeLast();
-    assert(node.tagName == "script");
+    assert(node.localName == "script");
     parser.phase = parser.originalPhase;
     //The rest of this method is all stuff that only happens if
     //document.write works
@@ -2009,8 +2010,8 @@
   // helper methods
   void clearStackToTableContext() {
     // "clear the stack back to a table context"
-    while (tree.openElements.last.tagName != "table" &&
-           tree.openElements.last.tagName != "html") {
+    while (tree.openElements.last.localName != "table" &&
+           tree.openElements.last.localName != "html") {
       //parser.parseError(token.span, "unexpected-implied-end-tag-in-table",
       //  {"name":  tree.openElements.last.name})
       tree.openElements.removeLast();
@@ -2021,7 +2022,7 @@
   // processing methods
   bool processEOF() {
     var last = tree.openElements.last;
-    if (last.tagName != "html") {
+    if (last.localName != "html") {
       parser.parseError(last.sourceSpan, "eof-in-table");
     } else {
       assert(parser.innerHTMLMode);
@@ -2130,11 +2131,11 @@
     if (tree.elementInScope("table", variant: "table")) {
       tree.generateImpliedEndTags();
       var last = tree.openElements.last;
-      if (last.tagName != "table") {
+      if (last.localName != "table") {
         parser.parseError(token.span, "end-tag-too-early-named",
-            {"gotName": "table", "expectedName": last.tagName});
+            {"gotName": "table", "expectedName": last.localName});
       }
-      while (tree.openElements.last.tagName != "table") {
+      while (tree.openElements.last.localName != "table") {
         tree.openElements.removeLast();
       }
       tree.openElements.removeLast();
@@ -2287,12 +2288,12 @@
     if (!ignoreEndTagCaption()) {
       // AT this code is quite similar to endTagTable in "InTable"
       tree.generateImpliedEndTags();
-      if (tree.openElements.last.tagName != "caption") {
+      if (tree.openElements.last.localName != "caption") {
         parser.parseError(token.span, "expected-one-end-tag-but-got-another",
           {"gotName": "caption",
-           "expectedName": tree.openElements.last.tagName});
+           "expectedName": tree.openElements.last.localName});
       }
-      while (tree.openElements.last.tagName != "caption") {
+      while (tree.openElements.last.localName != "caption") {
         tree.openElements.removeLast();
       }
       tree.openElements.removeLast();
@@ -2346,7 +2347,7 @@
   }
 
   bool ignoreEndTagColgroup() {
-    return tree.openElements.last.tagName == "html";
+    return tree.openElements.last.localName == "html";
   }
 
   bool processEOF() {
@@ -2431,12 +2432,12 @@
   // helper methods
   void clearStackToTableBodyContext() {
     var tableTags = const ["tbody", "tfoot", "thead", "html"];
-    while (!tableTags.contains(tree.openElements.last.tagName)) {
+    while (!tableTags.contains(tree.openElements.last.localName)) {
       //XXX parser.parseError(token.span, "unexpected-implied-end-tag-in-table",
       //  {"name": tree.openElements.last.name})
       tree.openElements.removeLast();
     }
-    if (tree.openElements.last.tagName == "html") {
+    if (tree.openElements.last.localName == "html") {
       assert(parser.innerHTMLMode);
     }
   }
@@ -2491,7 +2492,7 @@
         tree.elementInScope("thead", variant: "table") ||
         tree.elementInScope("tfoot", variant: "table")) {
       clearStackToTableBodyContext();
-      endTagTableRowGroup(new EndTagToken(tree.openElements.last.tagName));
+      endTagTableRowGroup(new EndTagToken(tree.openElements.last.localName));
       return token;
     } else {
       // innerHTML case
@@ -2544,11 +2545,11 @@
   void clearStackToTableRowContext() {
     while (true) {
       var last = tree.openElements.last;
-      if (last.tagName == "tr" || last.tagName == "html") break;
+      if (last.localName == "tr" || last.localName == "html") break;
 
       parser.parseError(last.sourceSpan,
           "unexpected-implied-end-tag-in-table-row",
-          {"name": tree.openElements.last.tagName});
+          {"name": tree.openElements.last.localName});
       tree.openElements.removeLast();
     }
   }
@@ -2694,7 +2695,7 @@
   void endTagTableCell(EndTagToken token) {
     if (tree.elementInScope(token.name, variant: "table")) {
       tree.generateImpliedEndTags(token.name);
-      if (tree.openElements.last.tagName != token.name) {
+      if (tree.openElements.last.localName != token.name) {
         parser.parseError(token.span, "unexpected-cell-end-tag",
             {"name": token.name});
         popOpenElementsUntil(token.name);
@@ -2756,7 +2757,7 @@
   // http://www.whatwg.org/specs/web-apps/current-work///in-select
   bool processEOF() {
     var last = tree.openElements.last;
-    if (last.tagName != "html") {
+    if (last.localName != "html") {
       parser.parseError(last.sourceSpan, "eof-in-select");
     } else {
       assert(parser.innerHTMLMode);
@@ -2774,17 +2775,17 @@
 
   void startTagOption(StartTagToken token) {
     // We need to imply </option> if <option> is the current node.
-    if (tree.openElements.last.tagName == "option") {
+    if (tree.openElements.last.localName == "option") {
       tree.openElements.removeLast();
     }
     tree.insertElement(token);
   }
 
   void startTagOptgroup(StartTagToken token) {
-    if (tree.openElements.last.tagName == "option") {
+    if (tree.openElements.last.localName == "option") {
       tree.openElements.removeLast();
     }
-    if (tree.openElements.last.tagName == "optgroup") {
+    if (tree.openElements.last.localName == "optgroup") {
       tree.openElements.removeLast();
     }
     tree.insertElement(token);
@@ -2817,7 +2818,7 @@
   }
 
   void endTagOption(EndTagToken token) {
-    if (tree.openElements.last.tagName == "option") {
+    if (tree.openElements.last.localName == "option") {
       tree.openElements.removeLast();
     } else {
       parser.parseError(token.span, "unexpected-end-tag-in-select",
@@ -2827,12 +2828,12 @@
 
   void endTagOptgroup(EndTagToken token) {
     // </optgroup> implicitly closes <option>
-    if (tree.openElements.last.tagName == "option" &&
-      tree.openElements[tree.openElements.length - 2].tagName == "optgroup") {
+    if (tree.openElements.last.localName == "option" &&
+      tree.openElements[tree.openElements.length - 2].localName == "optgroup") {
       tree.openElements.removeLast();
     }
     // It also closes </optgroup>
-    if (tree.openElements.last.tagName == "optgroup") {
+    if (tree.openElements.last.localName == "optgroup") {
       tree.openElements.removeLast();
     // But nothing else
     } else {
@@ -2995,7 +2996,7 @@
 
       parser.parseError(token.span,
           "unexpected-html-element-in-foreign-content", {'name': token.name});
-      while (tree.openElements.last.namespace !=
+      while (tree.openElements.last.namespaceUri !=
            tree.defaultNamespace &&
            !parser.isHTMLIntegrationPoint(tree.openElements.last) &&
            !parser.isMathMLTextIntegrationPoint(tree.openElements.last)) {
@@ -3004,14 +3005,14 @@
       return token;
 
     } else {
-      if (currentNode.namespace == Namespaces.mathml) {
+      if (currentNode.namespaceUri == Namespaces.mathml) {
         parser.adjustMathMLAttributes(token);
-      } else if (currentNode.namespace == Namespaces.svg) {
+      } else if (currentNode.namespaceUri == Namespaces.svg) {
         adjustSVGTagNames(token);
         parser.adjustSVGAttributes(token);
       }
       parser.adjustForeignAttributes(token);
-      token.namespace = currentNode.namespace;
+      token.namespace = currentNode.namespaceUri;
       tree.insertElement(token);
       if (token.selfClosing) {
         tree.openElements.removeLast();
@@ -3024,13 +3025,13 @@
   Token processEndTag(EndTagToken token) {
     var nodeIndex = tree.openElements.length - 1;
     var node = tree.openElements.last;
-    if (node.tagName != token.name) {
+    if (node.localName != token.name) {
       parser.parseError(token.span, "unexpected-end-tag", {"name": token.name});
     }
 
     var newToken = null;
     while (true) {
-      if (asciiUpper2Lower(node.tagName) == token.name) {
+      if (asciiUpper2Lower(node.localName) == token.name) {
         //XXX this isn't in the spec but it seems necessary
         if (parser.phase == parser._inTableTextPhase) {
           InTableTextPhase inTableText = parser.phase;
@@ -3046,7 +3047,7 @@
       nodeIndex -= 1;
 
       node = tree.openElements[nodeIndex];
-      if (node.namespace != tree.defaultNamespace) {
+      if (node.namespaceUri != tree.defaultNamespace) {
         continue;
       } else {
         newToken = parser.phase.processEndTag(token);
@@ -3137,7 +3138,7 @@
 
   bool processEOF() {
     var last = tree.openElements.last;
-    if (last.tagName != "html") {
+    if (last.localName != "html") {
       parser.parseError(last.sourceSpan, "eof-in-frameset");
     } else {
       assert(parser.innerHTMLMode);
@@ -3170,14 +3171,15 @@
   }
 
   void endTagFrameset(EndTagToken token) {
-    if (tree.openElements.last.tagName == "html") {
+    if (tree.openElements.last.localName == "html") {
       // innerHTML case
       parser.parseError(token.span,
           "unexpected-frameset-in-frameset-innerhtml");
     } else {
       tree.openElements.removeLast();
     }
-    if (!parser.innerHTMLMode && tree.openElements.last.tagName != "frameset") {
+    if (!parser.innerHTMLMode &&
+        tree.openElements.last.localName != "frameset") {
       // If we're not in innerHTML mode and the the current node is not a
       // "frameset" element (anymore) then switch.
       parser.phase = parser._afterFramesetPhase;
@@ -3355,3 +3357,11 @@
     return span.sourceUrl == null ? 'ParserError$res' : res;
   }
 }
+
+
+/// Convenience function to get the pair of namespace and localName.
+Pair<String, String> getElementNameTuple(Element e) {
+  var ns = e.namespaceUri;
+  if (ns == null) ns = Namespaces.html;
+  return new Pair(ns, e.localName);
+}
diff --git a/lib/src/tokenizer.dart b/lib/src/tokenizer.dart
index 24873ba..b486375 100644
--- a/lib/src/tokenizer.dart
+++ b/lib/src/tokenizer.dart
@@ -1287,7 +1287,7 @@
       }
     } else if (charStack.last == "[" &&
         parser != null && parser.tree.openElements.length > 0 &&
-        parser.tree.openElements.last.namespace
+        parser.tree.openElements.last.namespaceUri
             != parser.tree.defaultNamespace) {
       var matched = true;
       for (var expected in const ["C", "D", "A", "T", "A", "["]) {
diff --git a/lib/src/treebuilder.dart b/lib/src/treebuilder.dart
index 8c4d235..708c08c 100644
--- a/lib/src/treebuilder.dart
+++ b/lib/src/treebuilder.dart
@@ -3,6 +3,7 @@
 
 import 'dart:collection';
 import 'package:html5lib/dom.dart';
+import 'package:html5lib/parser.dart' show getElementNameTuple;
 import 'package:source_maps/span.dart' show FileSpan;
 import 'constants.dart';
 import 'list_proxy.dart';
@@ -14,18 +15,18 @@
 // from "leaking" into tables, object elements, and marquees.
 const Node Marker = null;
 
-// TODO(jmesserly): this should extend ListBase<Node>, but my simple attempt
+// TODO(jmesserly): this should extend ListBase<Element>, but my simple attempt
 // didn't work.
-class ActiveFormattingElements extends ListProxy<Node> {
+class ActiveFormattingElements extends ListProxy<Element> {
   ActiveFormattingElements() : super();
 
   // Override the "add" method.
   // TODO(jmesserly): I'd rather not override this; can we do this in the
   // calling code instead?
-  void add(Node node) {
+  void add(Element node) {
     int equalCount = 0;
     if (node != Marker) {
-      for (Node element in reversed) {
+      for (var element in reversed) {
         if (element == Marker) {
           break;
         }
@@ -61,8 +62,8 @@
 }
 
 
-bool _nodesEqual(Node node1, Node node2) {
-  return node1.nameTuple == node2.nameTuple &&
+bool _nodesEqual(Element node1, Element node2) {
+  return getElementNameTuple(node1) == getElementNameTuple(node2) &&
       _mapEquals(node1.attributes, node2.attributes);
 }
 
@@ -72,7 +73,7 @@
 
   Document document;
 
-  final openElements = <Node>[];
+  final List<Element> openElements = <Element>[];
 
   final activeFormattingElements = new ActiveFormattingElements();
 
@@ -105,7 +106,7 @@
   bool elementInScope(target, {String variant}) {
     //If we pass a node in we match that. if we pass a string
     //match any node with that name
-    bool exactNode = target is Node && target.nameTuple != null;
+    bool exactNode = target is Node;
 
     List listElements1 = scopingElements;
     List listElements2 = const [];
@@ -133,13 +134,13 @@
       }
     }
 
-    for (Node node in openElements.reversed) {
-      if (node.tagName == target && !exactNode ||
-          node == target && exactNode) {
+    for (var node in openElements.reversed) {
+      if (!exactNode && node.localName == target ||
+          exactNode && node == target) {
         return true;
       } else if (invert !=
-          (listElements1.contains(node.nameTuple) ||
-           listElements2.contains(node.nameTuple))) {
+          (listElements1.contains(getElementNameTuple(node)) ||
+           listElements2.contains(getElementNameTuple(node)))) {
         return false;
       }
     }
@@ -185,8 +186,8 @@
 
       // TODO(jmesserly): optimize this. No need to create a token.
       var cloneToken = new StartTagToken(
-          entry.tagName,
-          namespace: entry.namespace,
+          entry.localName,
+          namespace: entry.namespaceUri,
           data: new LinkedHashMap.from(entry.attributes))
           ..span = entry.sourceSpan;
 
@@ -213,13 +214,13 @@
   /// Check if an element exists between the end of the active
   /// formatting elements and the last marker. If it does, return it, else
   /// return null.
-  Node elementInActiveFormattingElements(String name) {
-    for (Node item in activeFormattingElements.reversed) {
+  Element elementInActiveFormattingElements(String name) {
+    for (var item in activeFormattingElements.reversed) {
       // Check for Marker first because if it's a Marker it doesn't have a
       // name attribute.
       if (item == Marker) {
         break;
-      } else if (item.tagName == name) {
+      } else if (item.localName == name) {
         return item;
       }
     }
@@ -276,7 +277,7 @@
   Element insertElementTable(token) {
     /// Create an element and insert it into the tree
     var element = createElement(token);
-    if (!tableInsertModeElements.contains(openElements.last.tagName)) {
+    if (!tableInsertModeElements.contains(openElements.last.localName)) {
       return insertElementNormal(token);
     } else {
       // We should be in the InTable mode. This means we want to do
@@ -300,7 +301,7 @@
     var parent = openElements.last;
 
     if (!insertFromTable || insertFromTable &&
-        !tableInsertModeElements.contains(openElements.last.tagName)) {
+        !tableInsertModeElements.contains(openElements.last.localName)) {
       _insertText(parent, data, span);
     } else {
       // We should be in the InTable mode. This means we want to do
@@ -347,8 +348,8 @@
     Node lastTable = null;
     Node fosterParent = null;
     var insertBefore = null;
-    for (Node elm in openElements.reversed) {
-      if (elm.tagName == "table") {
+    for (var elm in openElements.reversed) {
+      if (elm.localName == "table") {
         lastTable = elm;
         break;
       }
@@ -369,7 +370,7 @@
   }
 
   void generateImpliedEndTags([String exclude]) {
-    var name = openElements.last.tagName;
+    var name = openElements.last.localName;
     // XXX td, th and tr are not actually needed
     if (name != exclude && const ["dd", "dt", "li", "option", "optgroup", "p",
         "rp", "rt"].contains(name)) {
diff --git a/test/parser_feature_test.dart b/test/parser_feature_test.dart
index f96a8e2..212ae05 100644
--- a/test/parser_feature_test.dart
+++ b/test/parser_feature_test.dart
@@ -9,9 +9,9 @@
 
 main() {
   test('doctype is cloneable', () {
-    var doc = parse('<!DOCTYPE HTML>');
+    var doc = parse('<!doctype HTML>');
     DocumentType doctype = doc.nodes[0];
-    expect(doctype.clone().outerHtml, '<!DOCTYPE html>');
+    expect(doctype.clone().toString(), '<!DOCTYPE html>');
   });
 
   test('line counter', () {
@@ -22,12 +22,12 @@
 
   test('namespace html elements on', () {
     var doc = new HtmlParser('', tree: new TreeBuilder(true)).parse();
-    expect(doc.nodes[0].namespace, Namespaces.html);
+    expect(doc.nodes[0].namespaceUri, Namespaces.html);
   });
 
   test('namespace html elements off', () {
     var doc = new HtmlParser('', tree: new TreeBuilder(false)).parse();
-    expect(doc.nodes[0].namespace, null);
+    expect(doc.nodes[0].namespaceUri, null);
   });
 
   test('parse error spans - full', () {
@@ -156,60 +156,62 @@
 
   test('empty document has html, body, and head', () {
     var doc = parse('');
-    expect(doc.outerHtml, '<html><head></head><body></body></html>');
+    var html = '<html><head></head><body></body></html>';
+    expect(doc.outerHtml, html);
+    expect(doc.documentElement.outerHtml, html);
     expect(doc.head.outerHtml, '<head></head>');
     expect(doc.body.outerHtml, '<body></body>');
   });
 
   test('strange table case', () {
-    var doc = parseFragment('<table><tbody><foo>');
-    expect(doc.outerHtml, '<foo></foo><table><tbody></tbody></table>');
+    var doc = parse('<table><tbody><foo>').body;
+    expect(doc.innerHtml, '<foo></foo><table><tbody></tbody></table>');
   });
 
   group('html serialization', () {
     test('attribute order', () {
       // Note: the spec only requires a stable order.
       // However, we preserve the input order via LinkedHashMap
-      var doc = parseFragment('<foo d=1 a=2 c=3 b=4>');
-      expect(doc.outerHtml, '<foo d="1" a="2" c="3" b="4"></foo>');
-      expect(doc.querySelector('foo').attributes.remove('a'), '2');
-      expect(doc.outerHtml, '<foo d="1" c="3" b="4"></foo>');
-      doc.querySelector('foo').attributes['a'] = '0';
-      expect(doc.outerHtml, '<foo d="1" c="3" b="4" a="0"></foo>');
+      var body = parse('<foo d=1 a=2 c=3 b=4>').body;
+      expect(body.innerHtml, '<foo d="1" a="2" c="3" b="4"></foo>');
+      expect(body.querySelector('foo').attributes.remove('a'), '2');
+      expect(body.innerHtml, '<foo d="1" c="3" b="4"></foo>');
+      body.querySelector('foo').attributes['a'] = '0';
+      expect(body.innerHtml, '<foo d="1" c="3" b="4" a="0"></foo>');
     });
 
     test('escaping Text node in <script>', () {
-      var doc = parseFragment('<script>a && b</script>');
-      expect(doc.outerHtml, '<script>a && b</script>');
+      Element e = parseFragment('<script>a && b</script>').firstChild;
+      expect(e.outerHtml, '<script>a && b</script>');
     });
 
     test('escaping Text node in <span>', () {
-      var doc = parseFragment('<span>a && b</span>');
-      expect(doc.outerHtml, '<span>a &amp;&amp; b</span>');
+      Element e = parseFragment('<span>a && b</span>').firstChild;
+      expect(e.outerHtml, '<span>a &amp;&amp; b</span>');
     });
 
     test('Escaping attributes', () {
-      var doc = parseFragment('<div class="a<b>">');
-      expect(doc.outerHtml, '<div class="a<b>"></div>');
-      doc = parseFragment('<div class=\'a"b\'>');
-      expect(doc.outerHtml, '<div class="a&quot;b"></div>');
+      Element e = parseFragment('<div class="a<b>">').firstChild;
+      expect(e.outerHtml, '<div class="a<b>"></div>');
+      e = parseFragment('<div class=\'a"b\'>').firstChild;
+      expect(e.outerHtml, '<div class="a&quot;b"></div>');
     });
 
     test('Escaping non-breaking space', () {
       var text = '<span>foO\u00A0bar</span>';
       expect(text.codeUnitAt(text.indexOf('O') + 1), 0xA0);
-      var doc = parseFragment(text);
-      expect(doc.outerHtml, '<span>foO&nbsp;bar</span>');
+      Element e = parseFragment(text).firstChild;
+      expect(e.outerHtml, '<span>foO&nbsp;bar</span>');
     });
 
     test('Newline after <pre>', () {
-      var doc = parseFragment('<pre>\n\nsome text</span>');
-      expect(doc.querySelector('pre').nodes[0].data, '\nsome text');
-      expect(doc.outerHtml, '<pre>\n\nsome text</pre>');
+      Element e = parseFragment('<pre>\n\nsome text</span>').firstChild;
+      expect((e.firstChild as Text).data, '\nsome text');
+      expect(e.outerHtml, '<pre>\n\nsome text</pre>');
 
-      doc = parseFragment('<pre>\nsome text</span>');
-      expect(doc.querySelector('pre').nodes[0].data, 'some text');
-      expect(doc.outerHtml, '<pre>some text</pre>');
+      e = parseFragment('<pre>\nsome text</span>').firstChild;
+      expect((e.firstChild as Text).data, 'some text');
+      expect(e.outerHtml, '<pre>some text</pre>');
     });
 
     test('xml namespaces', () {