[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"><!DOCTYPE ${node.tagName}>'
+ _str.write('<code class="markup doctype"><!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('<<code class="markup element-name">${node.tagName}</code>');
+ final tag = node.localName;
+ _str.write('<<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(
- '</<code class="markup element-name">${node.tagName}</code>>');
+ '</<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 && b</span>');
+ Element e = parseFragment('<span>a && b</span>').firstChild;
+ expect(e.outerHtml, '<span>a && 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"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"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 bar</span>');
+ Element e = parseFragment(text).firstChild;
+ expect(e.outerHtml, '<span>foO 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', () {