| Index: pkg/third_party/html5lib/lib/src/treebuilder.dart
|
| diff --git a/pkg/third_party/html5lib/lib/src/treebuilder.dart b/pkg/third_party/html5lib/lib/src/treebuilder.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..bb53d58509881ed00855f02a284c94e7d19ed296
|
| --- /dev/null
|
| +++ b/pkg/third_party/html5lib/lib/src/treebuilder.dart
|
| @@ -0,0 +1,400 @@
|
| +/** Internals to the tree builders. */
|
| +library treebuilder;
|
| +
|
| +import 'dart:collection';
|
| +import 'package:html5lib/dom.dart';
|
| +import 'package:source_maps/span.dart' show FileSpan;
|
| +import 'constants.dart';
|
| +import 'list_proxy.dart';
|
| +import 'token.dart';
|
| +import 'utils.dart';
|
| +
|
| +// The scope markers are inserted when entering object elements,
|
| +// marquees, table cells, and table captions, and are used to prevent formatting
|
| +// from "leaking" into tables, object elements, and marquees.
|
| +final Node Marker = null;
|
| +
|
| +// TODO(jmesserly): this should extend ListBase<Node>, but my simple attempt
|
| +// didn't work.
|
| +class ActiveFormattingElements extends ListProxy<Node> {
|
| + 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) {
|
| + int equalCount = 0;
|
| + if (node != Marker) {
|
| + for (Node element in reversed) {
|
| + if (element == Marker) {
|
| + break;
|
| + }
|
| + if (_nodesEqual(element, node)) {
|
| + equalCount += 1;
|
| + }
|
| + if (equalCount == 3) {
|
| + remove(element);
|
| + break;
|
| + }
|
| + }
|
| + }
|
| + super.add(node);
|
| + }
|
| +}
|
| +
|
| +// TODO(jmesserly): this should exist in corelib...
|
| +bool _mapEquals(Map a, Map b) {
|
| + if (a.length != b.length) return false;
|
| + if (a.length == 0) return true;
|
| +
|
| + for (var keyA in a.keys) {
|
| + var valB = b[keyA];
|
| + if (valB == null && !b.containsKey(keyA)) {
|
| + return false;
|
| + }
|
| +
|
| + if (a[keyA] != valB) {
|
| + return false;
|
| + }
|
| + }
|
| + return true;
|
| +}
|
| +
|
| +
|
| +bool _nodesEqual(Node node1, Node node2) {
|
| + return node1.nameTuple == node2.nameTuple &&
|
| + _mapEquals(node1.attributes, node2.attributes);
|
| +}
|
| +
|
| +/** Basic treebuilder implementation. */
|
| +class TreeBuilder {
|
| + final String defaultNamespace;
|
| +
|
| + Document document;
|
| +
|
| + final openElements = <Node>[];
|
| +
|
| + final activeFormattingElements = new ActiveFormattingElements();
|
| +
|
| + Node headPointer;
|
| +
|
| + Node formPointer;
|
| +
|
| + /**
|
| + * Switch the function used to insert an element from the
|
| + * normal one to the misnested table one and back again
|
| + */
|
| + bool insertFromTable;
|
| +
|
| + TreeBuilder(bool namespaceHTMLElements)
|
| + : defaultNamespace = namespaceHTMLElements ? Namespaces.html : null {
|
| + reset();
|
| + }
|
| +
|
| + void reset() {
|
| + openElements.clear();
|
| + activeFormattingElements.clear();
|
| +
|
| + //XXX - rename these to headElement, formElement
|
| + headPointer = null;
|
| + formPointer = null;
|
| +
|
| + insertFromTable = false;
|
| +
|
| + document = new Document();
|
| + }
|
| +
|
| + 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;
|
| +
|
| + List listElements1 = scopingElements;
|
| + List listElements2 = const [];
|
| + bool invert = false;
|
| + if (variant != null) {
|
| + switch (variant) {
|
| + case "button":
|
| + listElements2 = const [const Pair(Namespaces.html, "button")];
|
| + break;
|
| + case "list":
|
| + listElements2 = const [const Pair(Namespaces.html, "ol"),
|
| + const Pair(Namespaces.html, "ul")];
|
| + break;
|
| + case "table":
|
| + listElements1 = const [const Pair(Namespaces.html, "html"),
|
| + const Pair(Namespaces.html, "table")];
|
| + break;
|
| + case "select":
|
| + listElements1 = const [const Pair(Namespaces.html, "optgroup"),
|
| + const Pair(Namespaces.html, "option")];
|
| + invert = true;
|
| + break;
|
| + default: assert(false); break;
|
| + }
|
| + }
|
| +
|
| + for (Node node in openElements.reversed) {
|
| + if (node.tagName == target && !exactNode ||
|
| + node == target && exactNode) {
|
| + return true;
|
| + } else if (invert !=
|
| + (listElements1.contains(node.nameTuple) ||
|
| + listElements2.contains(node.nameTuple))) {
|
| + return false;
|
| + }
|
| + }
|
| +
|
| + assert(false); // We should never reach this point
|
| + }
|
| +
|
| + void reconstructActiveFormattingElements() {
|
| + // Within this algorithm the order of steps described in the
|
| + // specification is not quite the same as the order of steps in the
|
| + // code. It should still do the same though.
|
| +
|
| + // Step 1: stop the algorithm when there's nothing to do.
|
| + if (activeFormattingElements.length == 0) {
|
| + return;
|
| + }
|
| +
|
| + // Step 2 and step 3: we start with the last element. So i is -1.
|
| + int i = activeFormattingElements.length - 1;
|
| + var entry = activeFormattingElements[i];
|
| + if (entry == Marker || openElements.contains(entry)) {
|
| + return;
|
| + }
|
| +
|
| + // Step 6
|
| + while (entry != Marker && !openElements.contains(entry)) {
|
| + if (i == 0) {
|
| + //This will be reset to 0 below
|
| + i = -1;
|
| + break;
|
| + }
|
| + i -= 1;
|
| + // Step 5: let entry be one earlier in the list.
|
| + entry = activeFormattingElements[i];
|
| + }
|
| +
|
| + while (true) {
|
| + // Step 7
|
| + i += 1;
|
| +
|
| + // Step 8
|
| + entry = activeFormattingElements[i];
|
| +
|
| + // TODO(jmesserly): optimize this. No need to create a token.
|
| + var cloneToken = new StartTagToken(
|
| + entry.tagName,
|
| + namespace: entry.namespace,
|
| + data: new LinkedHashMap.from(entry.attributes))
|
| + ..span = entry.sourceSpan;
|
| +
|
| + // Step 9
|
| + var element = insertElement(cloneToken);
|
| +
|
| + // Step 10
|
| + activeFormattingElements[i] = element;
|
| +
|
| + // Step 11
|
| + if (element == activeFormattingElements.last) {
|
| + break;
|
| + }
|
| + }
|
| + }
|
| +
|
| + void clearActiveFormattingElements() {
|
| + var entry = activeFormattingElements.removeLast();
|
| + while (activeFormattingElements.length > 0 && entry != Marker) {
|
| + entry = activeFormattingElements.removeLast();
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * 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) {
|
| + // 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) {
|
| + return item;
|
| + }
|
| + }
|
| + return null;
|
| + }
|
| +
|
| + void insertRoot(Token token) {
|
| + var element = createElement(token);
|
| + openElements.add(element);
|
| + document.nodes.add(element);
|
| + }
|
| +
|
| + void insertDoctype(DoctypeToken token) {
|
| + var doctype = new DocumentType(token.name, token.publicId, token.systemId)
|
| + ..sourceSpan = token.span;
|
| + document.nodes.add(doctype);
|
| + }
|
| +
|
| + void insertComment(StringToken token, [Node parent]) {
|
| + if (parent == null) {
|
| + parent = openElements.last;
|
| + }
|
| + parent.nodes.add(new Comment(token.data)..sourceSpan = token.span);
|
| + }
|
| +
|
| + /** Create an element but don't insert it anywhere */
|
| + Element createElement(StartTagToken token) {
|
| + var name = token.name;
|
| + var namespace = token.namespace;
|
| + if (namespace == null) namespace = defaultNamespace;
|
| + var element = new Element(name, namespace)
|
| + ..attributes = token.data
|
| + ..sourceSpan = token.span;
|
| + return element;
|
| + }
|
| +
|
| + Element insertElement(StartTagToken token) {
|
| + if (insertFromTable) return insertElementTable(token);
|
| + return insertElementNormal(token);
|
| + }
|
| +
|
| + Element insertElementNormal(StartTagToken token) {
|
| + var name = token.name;
|
| + var namespace = token.namespace;
|
| + if (namespace == null) namespace = defaultNamespace;
|
| + var element = new Element(name, namespace)
|
| + ..attributes = token.data
|
| + ..sourceSpan = token.span;
|
| + openElements.last.nodes.add(element);
|
| + openElements.add(element);
|
| + return element;
|
| + }
|
| +
|
| + Element insertElementTable(token) {
|
| + /** Create an element and insert it into the tree */
|
| + var element = createElement(token);
|
| + if (!tableInsertModeElements.contains(openElements.last.tagName)) {
|
| + return insertElementNormal(token);
|
| + } else {
|
| + // We should be in the InTable mode. This means we want to do
|
| + // special magic element rearranging
|
| + var nodePos = getTableMisnestedNodePosition();
|
| + if (nodePos[1] == null) {
|
| + // TODO(jmesserly): I don't think this is reachable. If insertFromTable
|
| + // is true, there will be a <table> element open, and it always has a
|
| + // parent pointer.
|
| + nodePos[0].nodes.add(element);
|
| + } else {
|
| + nodePos[0].insertBefore(element, nodePos[1]);
|
| + }
|
| + openElements.add(element);
|
| + }
|
| + return element;
|
| + }
|
| +
|
| + /** Insert text data. */
|
| + void insertText(String data, FileSpan span) {
|
| + var parent = openElements.last;
|
| +
|
| + if (!insertFromTable || insertFromTable &&
|
| + !tableInsertModeElements.contains(openElements.last.tagName)) {
|
| + _insertText(parent, data, span);
|
| + } else {
|
| + // We should be in the InTable mode. This means we want to do
|
| + // special magic element rearranging
|
| + var nodePos = getTableMisnestedNodePosition();
|
| + _insertText(nodePos[0], data, span, nodePos[1]);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Insert [data] as text in the current node, positioned before the
|
| + * start of node [refNode] or to the end of the node's text.
|
| + */
|
| + static void _insertText(Node parent, String data, FileSpan span,
|
| + [Element refNode]) {
|
| + var nodes = parent.nodes;
|
| + if (refNode == null) {
|
| + if (nodes.length > 0 && nodes.last is Text) {
|
| + Text last = nodes.last;
|
| + last.value = '${last.value}$data';
|
| +
|
| + if (span != null) {
|
| + last.sourceSpan = span.file.span(last.sourceSpan.start.offset,
|
| + span.end.offset);
|
| + }
|
| + } else {
|
| + nodes.add(new Text(data)..sourceSpan = span);
|
| + }
|
| + } else {
|
| + int index = nodes.indexOf(refNode);
|
| + if (index > 0 && nodes[index - 1] is Text) {
|
| + Text last = nodes[index - 1];
|
| + last.value = '${last.value}$data';
|
| + } else {
|
| + nodes.insert(index, new Text(data)..sourceSpan = span);
|
| + }
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Get the foster parent element, and sibling to insert before
|
| + * (or null) when inserting a misnested table node
|
| + */
|
| + List<Node> getTableMisnestedNodePosition() {
|
| + // The foster parent element is the one which comes before the most
|
| + // recently opened table element
|
| + // XXX - this is really inelegant
|
| + Node lastTable = null;
|
| + Node fosterParent = null;
|
| + var insertBefore = null;
|
| + for (Node elm in openElements.reversed) {
|
| + if (elm.tagName == "table") {
|
| + lastTable = elm;
|
| + break;
|
| + }
|
| + }
|
| + if (lastTable != null) {
|
| + // XXX - we should really check that this parent is actually a
|
| + // node here
|
| + if (lastTable.parent != null) {
|
| + fosterParent = lastTable.parent;
|
| + insertBefore = lastTable;
|
| + } else {
|
| + fosterParent = openElements[openElements.indexOf(lastTable) - 1];
|
| + }
|
| + } else {
|
| + fosterParent = openElements[0];
|
| + }
|
| + return [fosterParent, insertBefore];
|
| + }
|
| +
|
| + void generateImpliedEndTags([String exclude]) {
|
| + var name = openElements.last.tagName;
|
| + // XXX td, th and tr are not actually needed
|
| + if (name != exclude && const ["dd", "dt", "li", "option", "optgroup", "p",
|
| + "rp", "rt"].contains(name)) {
|
| + openElements.removeLast();
|
| + // XXX This is not entirely what the specification says. We should
|
| + // investigate it more closely.
|
| + generateImpliedEndTags(exclude);
|
| + }
|
| + }
|
| +
|
| + /** Return the final tree. */
|
| + Document getDocument() => document;
|
| +
|
| + /** Return the final fragment. */
|
| + DocumentFragment getFragment() {
|
| + //XXX assert innerHTML
|
| + var fragment = new DocumentFragment();
|
| + openElements[0].reparentChildren(fragment);
|
| + return fragment;
|
| + }
|
| +}
|
|
|