Add italics and underline
authorAryeh Gregor <AryehGregor+gitcommit@gmail.com>
Mon, 21 Feb 2011 14:54:12 -0700
changeset 6 2f1ebe466678
parent 5 256b8ba2af6a
child 7 c07184a3d893
Add italics and underline

Also reorganized by getting rid of the entirely misnamed test/
directory.
editcommands.html
implementation.html
source.html
test/bold.html
test/editcommands.html
test/support/diff_match_patch.js
test/support/document.html
test/support/test.js
test/support/testharness.css
test/support/testharness.js
test/support/testharnessreport.js
xrefs.json
--- a/editcommands.html	Mon Feb 21 14:25:51 2011 -0700
+++ b/editcommands.html	Mon Feb 21 14:54:12 2011 -0700
@@ -480,6 +480,27 @@
 <p class=XXX>b has font-weight: bolder, not font-weight: bold.  This produces
 unexpected behavior if there are font-weight: lighters or something thrown
 around.  Maybe that's not worth worrying about.
+
+<dt><code title=""><dfn id=command-italic title=command-italic>italic</dfn></code>
+<dd><p>If the <a href=#beginning-element>beginning element</a> of the <a href=http://html5.org/specs/dom-range.html#range><code class=external data-anolis-spec=domrange>Range</code></a> has font-style
+with computed value not equal to "italic" or "oblique", the user agent must
+<a href=#style-a-range title="style a range">style the <code class=external data-anolis-spec=domrange>Range</code></a> with <var title="">property
+name</var> "font-style", <var title="">property value</var> "italic", and <var title="">tag list</var> ["i", "em"].  Otherwise, it must <a href=#unstyle-a-range title="unstyle
+a range">unstyle it</a> with <var title="">property name</var> "font-style",
+<var title="">property value</var> "normal", and <var title="">tag list</var> ["i",
+"em"].
+
+<dt><code title=""><dfn id=command-underline title=command-underline>underline</dfn></code>
+<dd><p>If the <a href=#beginning-element>beginning element</a> of the <a href=http://html5.org/specs/dom-range.html#range><code class=external data-anolis-spec=domrange>Range</code></a> has
+text-decoration with a computed value that does not include "underline", the
+user agent must <a href=#style-a-range title="style a range">style the <code class=external data-anolis-spec=domrange>Range</code></a> with
+<var title="">property name</var> "text-decoration", <var title="">property
+value</var> "underline", and <var title="">tag list</var> ["u"].  Otherwise, it
+must <a href=#unstyle-a-range title="unstyle a range">unstyle it</a> with <var title="">property
+name</var> "text-decoration", <var title="">property value</var> "none", and <var title="">tag list</var> ["u"].
+
+<p class=XXX>This is wrong, it will clear strikethrough and overline.  Also,
+computed style isn't useful, because text-decoration doesn't inherit.
 </dl>
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/implementation.html	Mon Feb 21 14:54:12 2011 -0700
@@ -0,0 +1,550 @@
+<!doctype html>
+<title>execCommand() experimental implementation</title>
+<p>Spec:
+<button onclick=bold()><b>B</b></button>
+<button onclick=italic()><i>I</i></button>
+<button onclick=underline()><u>U</u></button>
+
+<p>Actual execCommand():
+<button onclick="execCommand('bold', false, null)"><b>B</b></button>
+<button onclick="execCommand('italic', false, null)"><i>I</i></button>
+<button onclick="execCommand('underline', false, null)"><u>U</u></button>
+<div contenteditable=true id=test>
+	<br> <br>
+	Some simple text<br>
+	<span>Some more text</span><br>
+	<b>Some more text</b><br>
+	<strong>Some more text</strong><br>
+	<span style=font-weight:bold>Some more text</span><br>
+	<span style=font-weight:bolder>Some more text</span><br>
+	<span style=font-weight:lighter>Some more text</span><br>
+	<span style=font-weight:900>Some more text</span><br>
+	<span style=font-weight:100>Some more text</span><br>
+	<i>Some more text</i><br>
+	<span style=font-style:italic>Some more text</span><br>
+	<span style=font-style:italic;font-weight:bold>Some more text</span><br>
+	<em style=font-weight:bold>Some more text</em><br>
+	<b>Some <span style=font-weight:200>more <b>te<span style=font-weight:bold>xt<strong>!</strong></span></b></span></b><br>
+	<p>Some simple text
+	<p><span>Some more text</span>
+	<p><b>Some more text</b>
+	<p><strong>Some more text</strong>
+	<p><span style=font-weight:bold>Some more text</span>
+	<p><span style=font-weight:bolder>Some more text</span>
+	<p><span style=font-weight:lighter>Some more text</span>
+	<p><span style=font-weight:900>Some more text</span>
+	<p><span style=font-weight:100>Some more text</span>
+	<p><i>Some more text</i>
+	<p><span style=font-style:italic>Some more text</span>
+	<p><span style=font-style:italic;font-weight:bold>Some more text</span>
+	<p><em style=font-weight:bold>Some more text</em>
+	<p><b>Some <span style=font-weight:200>more <b>te<span style=font-weight:bold>xt<strong>!</strong></span></b></span></b>
+</div>
+<script>
+"use strict";
+
+var htmlNamespace = "http://www.w3.org/1999/xhtml";
+
+function indexOf(node) {
+	var ret = 0;
+	while (node != node.parentNode.childNodes[ret]) {
+		ret++;
+	}
+	return ret;
+}
+
+function nodeLength(node) {
+	if (node.nodeType == Node.TEXT_NODE
+	|| node.nodeType == Node.COMMENT_NODE
+	|| node.nodeType == Node.PROCESSING_INSTRUCTION_NODE) {
+		return node.data.length;
+	}
+
+	return node.childNodes.length;
+}
+
+function nextNode(node) {
+	if (node.hasChildNodes()) {
+		return node.firstChild;
+	}
+	return nextNodeDescendants(node);
+}
+
+function previousNode(node) {
+	if (node.previousSibling) {
+		node = node.previousSibling;
+		while (node.hasChildNodes()) {
+			node = node.lastChild;
+		}
+		return node;
+	}
+	if (node.parentNode
+	&& node.parentNode.nodeType == Node.ELEMENT_NODE) {
+		return node.parentNode;
+	}
+	return null;
+}
+
+function nextNodeDescendants(node) {
+	while (node && !node.nextSibling) {
+		node = node.parentNode;
+	}
+	if (!node) {
+		return null;
+	}
+	return node.nextSibling;
+}
+
+function convertProperty(propertyName) {
+	// Special-case for now
+	var map = {
+		"fontStyle": "font-style",
+		"fontWeight": "font-weight",
+		"textDecoration": "text-decoration",
+	};
+	return map[propertyName];
+}
+
+
+function firstNode(range) {
+	// "If range's start offset is equal to the length of its start node,
+	// return the first Node that is after the start node and all its
+	// descendants (if any) in tree order. If there is no such Node, return the
+	// last Node in the document."
+	if (range.startOffset == nodeLength(range.startContainer)) {
+		var ret = nextNodeDescendants(range.startContainer);
+		if (!ret) {
+			ret = range.startContainer;
+			while (ret.hasChildNodes()) {
+				ret = ret.childNodes[ret.childNodes.length - 1];
+			}
+		}
+		return ret;
+	}
+
+	// "If range's start node is a Text, Comment, or ProcessingInstruction
+	// node, return that."
+	if (range.startContainer.nodeType == Node.TEXT_NODE
+	|| range.startContainer.nodeType == Node.COMMENT_NODE
+	|| range.startContainer.nodeType == Node.PROCESSING_INSTRUCTION_NODE) {
+		return range.startContainer;
+	}
+
+	// "If range's start node has children, return the child with index equal
+	// to the start offset."
+	if (range.startContainer.hasChildNodes()) {
+		return range.startContainer.childNodes[range.startOffset];
+	}
+
+	// "Return range's start node."
+	return range.startContainer;
+}
+
+function beginningElement(range) {
+	var first = firstNode(range);
+	if (first.nodeType == Node.ELEMENT_NODE) {
+		return first;
+	}
+
+	if (first.parentNode.nodeType == Node.ELEMENT_NODE) {
+		return first.parentNode;
+	}
+
+	return null;
+}
+
+function decomposeRange(range) {
+	// "Let start node, start offset, end node, and end offset be the start and
+	// end nodes and offsets of range, respectively."
+	var startNode = range.startContainer;
+	var startOffset = range.startOffset;
+	var endNode = range.endContainer;
+	var endOffset = range.endOffset;
+
+	// "If start node or end node is not an Element, Text,
+	// ProcessingInstruction, or Comment node, or is not an Element and has no
+	// parent, abort these steps."
+	// Skip the sanity check about node types/detached non-elements
+
+	// "If start node and end node are both Text nodes, and start node is the
+	// same as end node, and neither start offset nor end offset is equal to 0
+	// or the length of start node:"
+	if (startNode.nodeType == Node.TEXT_NODE
+	&& endNode.nodeType == Node.TEXT_NODE
+	&& startNode.isSameNode(endNode)
+	&& startOffset != 0
+	&& startOffset != startNode.data.length
+	&& endOffset != 0
+	&& endOffset != startNode.data.length) {
+		// "Run splitText(start offset) on start node and set start node to the
+		// result."
+		startNode = startNode.splitText(startOffset);
+
+		// "Run splitText(end offset − start offset) on start node and set
+		// start node to the previous sibling of the result."
+		startNode = startNode.splitText(endOffset - startOffset).previousSibling;
+
+		// "Return the list consisting of the single Node start node, and abort
+		// these steps."
+		return [startNode];
+	}
+
+	// "If start node is a Text node and start offset is neither 0 nor the
+	// length of start node, run splitText(start offset) on start node and set
+	// start node to the returned node. Set start offset to 0."
+	if (startNode.nodeType == Node.TEXT_NODE
+	&& startOffset != 0
+	&& startOffset != startNode.data.length) {
+		startNode = startNode.splitText(startOffset);
+		startOffset = 0;
+	}
+
+	// "If end node is a Text node and end offset is neither 0 nor the length
+	// of end node, run splitText(end offset) on end node and set end node to
+	// the previous sibling of the returned node. Set end offset to the length
+	// of the new end node."
+	if (endNode.nodeType == Node.TEXT_NODE
+	&& endOffset != 0
+	&& endOffset != endNode.data.length) {
+		endNode = endNode.splitText(endOffset).previousSibling;
+		endOffset = endNode.data.length;
+	}
+
+	var node;
+	// "If start node is an Element with at least one child, let node be the
+	// child of start node with index start offset."
+	if (startNode.nodeType == Node.ELEMENT_NODE
+	&& startNode.hasChildNodes()) {
+		node = startNode.childNodes[startOffset];
+	// "Otherwise, if start node is a Text node and start offset is its length,
+	// let node be the first Node after start node in tree order."
+	} else if (startNode.nodeType == Node.TEXT_NODE
+	&& startOffset == startNode.data.length) {
+		node = nextNode(startNode);
+	// "Otherwise, let node be start node."
+	} else {
+		node = startNode;
+	}
+
+	var end;
+	// "If end node is an Element and end offset is not 0, let end be the child
+	// of end node with index end offset − 1."
+	if (endNode.nodeType == Node.ELEMENT_NODE && endOffset != 0) {
+		end = endNode.childNodes[endOffset - 1];
+	// "Otherwise, if end offset is 0, let end be the first Node before end
+	// node in tree order."
+	} else if (endOffset == 0) {
+		end = previousNode(endNode);
+	// "Otherwise, let end be end node."
+	} else {
+		end = endNode;
+	}
+
+	// "While node is the first child of its parent and end is not a descendant
+	// of node's parent, set node to its parent."
+	while (node == node.parentNode.firstChild
+	&& !(end.compareDocumentPosition(node.parentNode) & Node.DOCUMENT_POSITION_CONTAINS)) {
+		node = node.parentNode;
+	}
+
+	// "While end is the last child of its parent and node is not a descendant
+	// of end's parent, set end to its parent."
+	while (end == end.parentNode.lastChild
+	&& !(node.compareDocumentPosition(end.parentNode) & Node.DOCUMENT_POSITION_CONTAINS)) {
+		end = end.parentNode;
+	}
+
+	// "Let node list be an empty list of Nodes."
+	var nodeList = [];
+
+	// "While node is not after end in tree order:"
+	while (!(end.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING)) {
+		// "Append node to node list."
+		nodeList.push(node);
+
+		// "Set node to the first Node in tree order that is after node and (if
+		// applicable) all its descendants. If no such Node exists, break out
+		// of these substeps."
+		node = nextNodeDescendants(node);
+		if (!node) {
+			break;
+		}
+
+		// "While node is an ancestor of end, set node to its first child."
+		while (end.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_CONTAINS) {
+			node = node.firstChild;
+		}
+	}
+
+	return nodeList;
+}
+
+function unstyleElement(element, propertyName, tagList) {
+	// "Let element children be the Element children of element."
+	var elementChildren = [];
+	for (var j = 0; j < element.childNodes.length; j++) {
+		if (element.childNodes[j].nodeType == Node.ELEMENT_NODE) {
+			elementChildren.push(element.childNodes[j]);
+		}
+	}
+
+	// "Unstyle each Element in element children, in order."
+	for (var j = 0; j < elementChildren.length; j++) {
+		unstyleElement(elementChildren[j], propertyName, tagList);
+	}
+
+	// "Let children be an empty list of Nodes."
+	var children = [];
+
+	// "If either
+	//
+	// * element is an HTML element with name either "span" or in tag list, and
+	//   it has only a single attribute, and that attribute is named "style",
+	//   and that style attribute sets only the CSS property property name; or
+	//
+	// * element is an HTML element with name in tag list and it has no
+	//   attributes,
+	//
+	// then:"
+	if (
+		(element.namespaceURI == htmlNamespace
+		&& (element.nodeName == "SPAN" || tagList.indexOf(element.nodeName.toLowerCase()) != -1)
+		&& element.attributes.length == 1
+		&& element.attributes[0].localName == "style"
+		&& element.style.length == 1
+		&& element.style.item(0) == convertProperty(propertyName)
+		)
+		||
+		(element.namespaceURI == htmlNamespace
+		&& tagList.indexOf(element.nodeName.toLowerCase()) != -1
+		&& element.attributes.length == 0)
+	) {
+		// "While element has children:"
+		while (element.hasChildNodes()) {
+			// "Let child be the first child of element."
+			var child = element.firstChild;
+
+			// "Append child to children."
+			children.push(child);
+
+			// "Insert child as the previous sibling of element."
+			element.parentNode.insertBefore(child, element);
+		}
+
+		// "Remove element."
+		element.parentNode.removeChild(element);
+
+		// "Return children and abort this algorithm."
+		return children;
+	}
+
+	// "Unset the CSS property property name of element."
+	element.style[propertyName] = '';
+	if (element.getAttribute("style") == "") {
+		element.removeAttribute("style");
+	}
+
+	// "If element is an HTML element with name in tag list:"
+	if (element.namespaceURI == htmlNamespace
+	&& tagList.indexOf(element.tagName.toLowerCase()) != -1) {
+		// "Let new element be a new HTML element with name "span", with the
+		// same attributes and ownerDocument as element."
+		var newElement = element.ownerDocument.createElement("span");
+		for (var j = 0; j < element.attributes.length; j++) {
+			// FIXME: Namespaces?
+			newElement.setAttribute(element.attributes[j].localName, element.attributes[j].value);
+		}
+
+		// "Append new element to element's parent as the previous sibling of
+		// element."
+		element.parentNode.insertBefore(newElement, element);
+
+		// "While element has children:"
+		while (element.hasChildNodes()) {
+			// "Let child be the first child of element."
+			var child = element.firstChild;
+
+			// "Append child to children."
+			children.push(child);
+
+			// "Append child as the last child of new element."
+			newElement.appendChild(child);
+		}
+
+		// "Remove element."
+		element.parentNode.removeChild(element);
+	}
+
+	// "Return children."
+	return children;
+}
+
+function styleRange(range, propertyName, propertyValue, tagList) {
+	// "Let node list be the result of decomposing range."
+	var nodeList = decomposeRange(range);
+
+	// "For each node in node list, in tree order:"
+	for (var i = 0; i < nodeList.length; i++) {
+		var node = nodeList[i];
+		// "If node is an Element:"
+		if (node.nodeType == Node.ELEMENT_NODE) {
+			// "If node is an HTML element with name in tag list, unset the CSS
+			// property property name of node. Otherwise, set the CSS property
+			// property name of node to property value."
+			if (node.namespaceURI == htmlNamespace
+			&& tagList.indexOf(node.tagName.toLowerCase()) != -1) {
+				node.style[propertyName] = '';
+				if (node.getAttribute("style") == "") {
+					node.removeAttribute("style");
+				}
+			} else {
+				node.style[propertyName] = propertyValue;
+			}
+
+			// "Let element children be the Element children of node."
+			var elementChildren = [];
+			for (var j = 0; j < node.childNodes.length; j++) {
+				if (node.childNodes[j].nodeType == Node.ELEMENT_NODE) {
+					elementChildren.push(node.childNodes[j]);
+				}
+			}
+
+			// "Unstyle each Element in element children, in order."
+			for (var j = 0; j < elementChildren.length; j++) {
+				unstyleElement(elementChildren[j], propertyName, tagList);
+			}
+		// "Otherwise, if node is a Text node:"
+		} else if (node.nodeType == Node.TEXT_NODE) {
+			var newParent;
+			// "If the previous sibling of node is an HTML element with local
+			// name in tag list with no attributes, let new parent equal the
+			// previous sibling of node."
+			if (node.previousSibling
+			&& node.previousSibling.nodeType == Node.ELEMENT_NODE
+			&& node.previousSibling.namespaceURI == htmlNamespace
+			&& tagList.indexOf(node.previousSibling.tagName.toLowerCase()) != -1
+			&& node.previousSibling.attributes.length == 0) {
+				newParent = node.previousSibling;
+			} else {
+				// "Otherwise, let new parent be a new HTML element with local
+				// name equal to the first string in tag list, with no
+				// attributes, and ownerDocument the same as node. Append new
+				// parent to node's parent as the previous sibling of node."
+				newParent = node.ownerDocument.createElement(tagList[0]);
+				node.parentNode.insertBefore(newParent, node);
+			}
+
+			// "Append node to new parent as its last child."
+			newParent.appendChild(node);
+		}
+		// "Otherwise, do nothing."
+	}
+}
+
+// Note: because browsers are inconsistent about what to return for computed
+// styles for bold, I'm making propertyValue an array in the implementation.
+function unstyleRange(range, propertyName, propertyValue, tagList) {
+	// "Let node list be the result of decomposing range."
+	var nodeList = decomposeRange(range);
+
+	// "For each node in node list, in order:"
+	for (var i = 0; i < nodeList.length; i++) {
+		var node = nodeList[i];
+
+		// "If node is an Element:"
+		if (node.nodeType == Node.ELEMENT_NODE) {
+			// "Let children be the result of unstyling node."
+			var children = unstyleElement(node, propertyName, tagList);
+
+			// "If node no longer has a parent:"
+			if (!node.parentNode) {
+				// "Insert all the Nodes in children into node list immediately
+				// after node, in order."
+				//
+				// splice() would be perfect, but it requires varargs.  :(
+				nodeList = nodeList.slice(0, i + 1)
+					.concat(children)
+					.concat(nodeList.slice(i + 1));
+
+				// "Continue with the next Node in node list, if any."
+				continue;
+			}
+
+			// "If the computed value of property name for node is not property
+			// value, set the CSS property property name of node to property
+			// value."
+			if (propertyValue.indexOf(getComputedStyle(node)[propertyName]) == -1) {
+				node.style[propertyName] = propertyValue[0];
+			}
+
+			// "Let element children be the Element children of node."
+			var elementChildren = [];
+			for (var j = 0; j < node.childNodes.length; j++) {
+				if (node.childNodes[j].nodeType == Node.ELEMENT_NODE) {
+					elementChildren.push(node.childNodes[j]);
+				}
+			}
+
+			// "Unstyle each Element in element children, in order."
+			for (var j = 0; j < elementChildren.length; j++) {
+				unstyleElement(elementChildren[j], propertyName, tagList);
+			}
+		// "Otherwise, if node is a Text node and the computed value of
+		// property name for node's parent is not property value:"
+		} else if (node.nodeType == Node.TEXT_NODE
+		&& propertyValue.indexOf(getComputedStyle(node.parentNode)[propertyName]) == -1) {
+			// "Let new parent be a new HTML element with name "span", with no
+			// attributes, and with ownerDocument equal to node's."
+			var newParent = node.ownerDocument.createElement("span");
+
+			// "Set the CSS property property name of new parent to property
+			// value."
+			newParent.style[propertyName] = propertyValue[0];
+
+			// "Insert new parent as node's previous sibling."
+			node.parentNode.insertBefore(newParent, node);
+
+			// "Append node to new parent as its child."
+			newParent.appendChild(node);
+		}
+		// "Otherwise, do nothing."
+	}
+}
+
+function bold() {
+	var selection = getSelection();
+	for (var i = 0; i < selection.rangeCount; i++) {
+		var fontWeight = getComputedStyle(beginningElement(selection.getRangeAt(i))).fontWeight;
+		if (fontWeight != "bold"
+		&& (!/^[0-9]+$/.test(fontWeight) || fontWeight < 700)) {
+			styleRange(selection.getRangeAt(i), "fontWeight", "bold", ["b", "strong"]);
+		} else {
+			unstyleRange(selection.getRangeAt(i), "fontWeight", ["normal", "400"], ["b", "strong"]);
+		}
+	}
+}
+
+function italic() {
+	var selection = getSelection();
+	for (var i = 0; i < selection.rangeCount; i++) {
+		var fontStyle = getComputedStyle(beginningElement(selection.getRangeAt(i))).fontStyle;
+		if (fontStyle != "italic" && fontStyle != "oblique") {
+			styleRange(selection.getRangeAt(i), "fontStyle", "italic", ["i", "em"]);
+		} else {
+			unstyleRange(selection.getRangeAt(i), "fontStyle", ["normal"], ["i", "em"]);
+		}
+	}
+}
+
+function underline() {
+	var selection = getSelection();
+	for (var i = 0; i < selection.rangeCount; i++) {
+		var textDecoration = getComputedStyle(beginningElement(selection.getRangeAt(i))).textDecoration;
+		if (!/ underline /.test(" " + textDecoration + " ")) {
+			styleRange(selection.getRangeAt(i), "textDecoration", "underline", ["u"]);
+		} else {
+			unstyleRange(selection.getRangeAt(i), "textDecoration", ["none"], ["u"]);
+		}
+	}
+}
+</script>
--- a/source.html	Mon Feb 21 14:25:51 2011 -0700
+++ b/source.html	Mon Feb 21 14:54:12 2011 -0700
@@ -520,6 +520,29 @@
 <p class=XXX>b has font-weight: bolder, not font-weight: bold.  This produces
 unexpected behavior if there are font-weight: lighters or something thrown
 around.  Maybe that's not worth worrying about.
+
+<dt><code title><dfn title=command-italic>italic</dfn></code>
+<dd><p>If the <span>beginning element</span> of the [[range]] has font-style
+with computed value not equal to "italic" or "oblique", the user agent must
+<span title="style a range">style the [[range]]</span> with <var title>property
+name</var> "font-style", <var title>property value</var> "italic", and <var
+title>tag list</var> ["i", "em"].  Otherwise, it must <span title="unstyle
+a range">unstyle it</span> with <var title>property name</var> "font-style",
+<var title>property value</var> "normal", and <var title>tag list</var> ["i",
+"em"].
+
+<dt><code title><dfn title=command-underline>underline</dfn></code>
+<dd><p>If the <span>beginning element</span> of the [[range]] has
+text-decoration with a computed value that does not include "underline", the
+user agent must <span title="style a range">style the [[range]]</span> with
+<var title>property name</var> "text-decoration", <var title>property
+value</var> "underline", and <var title>tag list</var> ["u"].  Otherwise, it
+must <span title="unstyle a range">unstyle it</span> with <var title>property
+name</var> "text-decoration", <var title>property value</var> "none", and <var
+title>tag list</var> ["u"].
+
+<p class=XXX>This is wrong, it will clear strikethrough and overline.  Also,
+computed style isn't useful, because text-decoration doesn't inherit.
 </dl>
 
 
--- a/test/bold.html	Mon Feb 21 14:25:51 2011 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,513 +0,0 @@
-<!doctype html>
-<title>execCommand("bold") tests</title>
-<button onclick=bold()>Bold</button>
-<div contenteditable=true id=test>
-	<br> <br>
-	Some simple text<br>
-	<span>Some more text</span><br>
-	<b>Some more text</b><br>
-	<strong>Some more text</strong><br>
-	<span style=font-weight:bold>Some more text</span><br>
-	<span style=font-weight:bolder>Some more text</span><br>
-	<span style=font-weight:lighter>Some more text</span><br>
-	<span style=font-weight:900>Some more text</span><br>
-	<span style=font-weight:100>Some more text</span><br>
-	<i>Some more text</i><br>
-	<span style=font-style:italic>Some more text</span><br>
-	<span style=font-style:italic;font-weight:bold>Some more text</span><br>
-	<em style=font-weight:bold>Some more text</em><br>
-	<b>Some <span style=font-weight:200>more <b>te<span style=font-weight:bold>xt<strong>!</strong></span></b></span></b><br>
-	<p>Some simple text
-	<p><span>Some more text</span>
-	<p><b>Some more text</b>
-	<p><strong>Some more text</strong>
-	<p><span style=font-weight:bold>Some more text</span>
-	<p><span style=font-weight:bolder>Some more text</span>
-	<p><span style=font-weight:lighter>Some more text</span>
-	<p><span style=font-weight:900>Some more text</span>
-	<p><span style=font-weight:100>Some more text</span>
-	<p><i>Some more text</i>
-	<p><span style=font-style:italic>Some more text</span>
-	<p><span style=font-style:italic;font-weight:bold>Some more text</span>
-	<p><em style=font-weight:bold>Some more text</em>
-	<p><b>Some <span style=font-weight:200>more <b>te<span style=font-weight:bold>xt<strong>!</strong></span></b></span></b>
-</div>
-<script>
-"use strict";
-
-var htmlNamespace = "http://www.w3.org/1999/xhtml";
-
-function indexOf(node) {
-	var ret = 0;
-	while (node != node.parentNode.childNodes[ret]) {
-		ret++;
-	}
-	return ret;
-}
-
-function nodeLength(node) {
-	if (node.nodeType == Node.TEXT_NODE
-	|| node.nodeType == Node.COMMENT_NODE
-	|| node.nodeType == Node.PROCESSING_INSTRUCTION_NODE) {
-		return node.data.length;
-	}
-
-	return node.childNodes.length;
-}
-
-function nextNode(node) {
-	if (node.hasChildNodes()) {
-		return node.firstChild;
-	}
-	return nextNodeDescendants(node);
-}
-
-function previousNode(node) {
-	if (node.previousSibling) {
-		node = node.previousSibling;
-		while (node.hasChildNodes()) {
-			node = node.lastChild;
-		}
-		return node;
-	}
-	if (node.parentNode
-	&& node.parentNode.nodeType == Node.ELEMENT_NODE) {
-		return node.parentNode;
-	}
-	return null;
-}
-
-function nextNodeDescendants(node) {
-	while (node && !node.nextSibling) {
-		node = node.parentNode;
-	}
-	if (!node) {
-		return null;
-	}
-	return node.nextSibling;
-}
-
-function convertProperty(propertyName) {
-	// Hack for now
-	return "font-weight";
-}
-
-
-function firstNode(range) {
-	// "If range's start offset is equal to the length of its start node,
-	// return the first Node that is after the start node and all its
-	// descendants (if any) in tree order. If there is no such Node, return the
-	// last Node in the document."
-	if (range.startOffset == nodeLength(range.startContainer)) {
-		var ret = nextNodeDescendants(range.startContainer);
-		if (!ret) {
-			ret = range.startContainer;
-			while (ret.hasChildNodes()) {
-				ret = ret.childNodes[ret.childNodes.length - 1];
-			}
-		}
-		return ret;
-	}
-
-	// "If range's start node is a Text, Comment, or ProcessingInstruction
-	// node, return that."
-	if (range.startContainer.nodeType == Node.TEXT_NODE
-	|| range.startContainer.nodeType == Node.COMMENT_NODE
-	|| range.startContainer.nodeType == Node.PROCESSING_INSTRUCTION_NODE) {
-		return range.startContainer;
-	}
-
-	// "If range's start node has children, return the child with index equal
-	// to the start offset."
-	if (range.startContainer.hasChildNodes()) {
-		return range.startContainer.childNodes[range.startOffset];
-	}
-
-	// "Return range's start node."
-	return range.startContainer;
-}
-
-function beginningElement(range) {
-	var first = firstNode(range);
-	if (first.nodeType == Node.ELEMENT_NODE) {
-		return first;
-	}
-
-	if (first.parentNode.nodeType == Node.ELEMENT_NODE) {
-		return first.parentNode;
-	}
-
-	return null;
-}
-
-function decomposeRange(range) {
-	// "Let start node, start offset, end node, and end offset be the start and
-	// end nodes and offsets of range, respectively."
-	var startNode = range.startContainer;
-	var startOffset = range.startOffset;
-	var endNode = range.endContainer;
-	var endOffset = range.endOffset;
-
-	// "If start node or end node is not an Element, Text,
-	// ProcessingInstruction, or Comment node, or is not an Element and has no
-	// parent, abort these steps."
-	// Skip the sanity check about node types/detached non-elements
-
-	// "If start node and end node are both Text nodes, and start node is the
-	// same as end node, and neither start offset nor end offset is equal to 0
-	// or the length of start node:"
-	if (startNode.nodeType == Node.TEXT_NODE
-	&& endNode.nodeType == Node.TEXT_NODE
-	&& startNode.isSameNode(endNode)
-	&& startOffset != 0
-	&& startOffset != startNode.data.length
-	&& endOffset != 0
-	&& endOffset != startNode.data.length) {
-		// "Run splitText(start offset) on start node and set start node to the
-		// result."
-		startNode = startNode.splitText(startOffset);
-
-		// "Run splitText(end offset − start offset) on start node and set
-		// start node to the previous sibling of the result."
-		startNode = startNode.splitText(endOffset - startOffset).previousSibling;
-
-		// "Return the list consisting of the single Node start node, and abort
-		// these steps."
-		return [startNode];
-	}
-
-	// "If start node is a Text node and start offset is neither 0 nor the
-	// length of start node, run splitText(start offset) on start node and set
-	// start node to the returned node. Set start offset to 0."
-	if (startNode.nodeType == Node.TEXT_NODE
-	&& startOffset != 0
-	&& startOffset != startNode.data.length) {
-		startNode = startNode.splitText(startOffset);
-		startOffset = 0;
-	}
-
-	// "If end node is a Text node and end offset is neither 0 nor the length
-	// of end node, run splitText(end offset) on end node and set end node to
-	// the previous sibling of the returned node. Set end offset to the length
-	// of the new end node."
-	if (endNode.nodeType == Node.TEXT_NODE
-	&& endOffset != 0
-	&& endOffset != endNode.data.length) {
-		endNode = endNode.splitText(endOffset).previousSibling;
-		endOffset = endNode.data.length;
-	}
-
-	var node;
-	// "If start node is an Element with at least one child, let node be the
-	// child of start node with index start offset."
-	if (startNode.nodeType == Node.ELEMENT_NODE
-	&& startNode.hasChildNodes()) {
-		node = startNode.childNodes[startOffset];
-	// "Otherwise, if start node is a Text node and start offset is its length,
-	// let node be the first Node after start node in tree order."
-	} else if (startNode.nodeType == Node.TEXT_NODE
-	&& startOffset == startNode.data.length) {
-		node = nextNode(startNode);
-	// "Otherwise, let node be start node."
-	} else {
-		node = startNode;
-	}
-
-	var end;
-	// "If end node is an Element and end offset is not 0, let end be the child
-	// of end node with index end offset − 1."
-	if (endNode.nodeType == Node.ELEMENT_NODE && endOffset != 0) {
-		end = endNode.childNodes[endOffset - 1];
-	// "Otherwise, if end offset is 0, let end be the first Node before end
-	// node in tree order."
-	} else if (endOffset == 0) {
-		end = previousNode(endNode);
-	// "Otherwise, let end be end node."
-	} else {
-		end = endNode;
-	}
-
-	// "While node is the first child of its parent and end is not a descendant
-	// of node's parent, set node to its parent."
-	while (node == node.parentNode.firstChild
-	&& !(end.compareDocumentPosition(node.parentNode) & Node.DOCUMENT_POSITION_CONTAINS)) {
-		node = node.parentNode;
-	}
-
-	// "While end is the last child of its parent and node is not a descendant
-	// of end's parent, set end to its parent."
-	while (end == end.parentNode.lastChild
-	&& !(node.compareDocumentPosition(end.parentNode) & Node.DOCUMENT_POSITION_CONTAINS)) {
-		end = end.parentNode;
-	}
-
-	// "Let node list be an empty list of Nodes."
-	var nodeList = [];
-
-	// "While node is not after end in tree order:"
-	while (!(end.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING)) {
-		// "Append node to node list."
-		nodeList.push(node);
-
-		// "Set node to the first Node in tree order that is after node and (if
-		// applicable) all its descendants. If no such Node exists, break out
-		// of these substeps."
-		node = nextNodeDescendants(node);
-		if (!node) {
-			break;
-		}
-
-		// "While node is an ancestor of end, set node to its first child."
-		while (end.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_CONTAINS) {
-			node = node.firstChild;
-		}
-	}
-
-	return nodeList;
-}
-
-function unstyleElement(element, propertyName, tagList) {
-	// "Let element children be the Element children of element."
-	var elementChildren = [];
-	for (var j = 0; j < element.childNodes.length; j++) {
-		if (element.childNodes[j].nodeType == Node.ELEMENT_NODE) {
-			elementChildren.push(element.childNodes[j]);
-		}
-	}
-
-	// "Unstyle each Element in element children, in order."
-	for (var j = 0; j < elementChildren.length; j++) {
-		unstyleElement(elementChildren[j], propertyName, tagList);
-	}
-
-	// "Let children be an empty list of Nodes."
-	var children = [];
-
-	// "If either
-	//
-	// * element is an HTML element with name either "span" or in tag list, and
-	//   it has only a single attribute, and that attribute is named "style",
-	//   and that style attribute sets only the CSS property property name; or
-	//
-	// * element is an HTML element with name in tag list and it has no
-	//   attributes,
-	//
-	// then:"
-	if (
-		(element.namespaceURI == htmlNamespace
-		&& (element.nodeName == "SPAN" || tagList.indexOf(element.nodeName.toLowerCase()) != -1)
-		&& element.attributes.length == 1
-		&& element.attributes[0].localName == "style"
-		&& element.style.length == 1
-		&& element.style.item(0) == convertProperty(propertyName)
-		)
-		||
-		(element.namespaceURI == htmlNamespace
-		&& tagList.indexOf(element.nodeName.toLowerCase()) != -1
-		&& element.attributes.length == 0)
-	) {
-		// "While element has children:"
-		while (element.hasChildNodes()) {
-			// "Let child be the first child of element."
-			var child = element.firstChild;
-
-			// "Append child to children."
-			children.push(child);
-
-			// "Insert child as the previous sibling of element."
-			element.parentNode.insertBefore(child, element);
-		}
-
-		// "Remove element."
-		element.parentNode.removeChild(element);
-
-		// "Return children and abort this algorithm."
-		return children;
-	}
-
-	// "Unset the CSS property property name of element."
-	element.style[propertyName] = '';
-	if (element.getAttribute("style") == "") {
-		element.removeAttribute("style");
-	}
-
-	// "If element is an HTML element with name in tag list:"
-	if (element.namespaceURI == htmlNamespace
-	&& tagList.indexOf(element.tagName.toLowerCase()) != -1) {
-		// "Let new element be a new HTML element with name "span", with the
-		// same attributes and ownerDocument as element."
-		var newElement = element.ownerDocument.createElement("span");
-		for (var j = 0; j < element.attributes.length; j++) {
-			// FIXME: Namespaces?
-			newElement.setAttribute(element.attributes[j].localName, element.attributes[j].value);
-		}
-
-		// "Append new element to element's parent as the previous sibling of
-		// element."
-		element.parentNode.insertBefore(newElement, element);
-
-		// "While element has children:"
-		while (element.hasChildNodes()) {
-			// "Let child be the first child of element."
-			var child = element.firstChild;
-
-			// "Append child to children."
-			children.push(child);
-
-			// "Append child as the last child of new element."
-			newElement.appendChild(child);
-		}
-
-		// "Remove element."
-		element.parentNode.removeChild(element);
-	}
-
-	// "Return children."
-	return children;
-}
-
-function styleRange(range, propertyName, propertyValue, tagList) {
-	// "Let node list be the result of decomposing range."
-	var nodeList = decomposeRange(range);
-
-	// "For each node in node list, in tree order:"
-	for (var i = 0; i < nodeList.length; i++) {
-		var node = nodeList[i];
-		// "If node is an Element:"
-		if (node.nodeType == Node.ELEMENT_NODE) {
-			// "If node is an HTML element with name in tag list, unset the CSS
-			// property property name of node. Otherwise, set the CSS property
-			// property name of node to property value."
-			if (node.namespaceURI == htmlNamespace
-			&& tagList.indexOf(node.tagName.toLowerCase()) != -1) {
-				node.style[propertyName] = '';
-				if (node.getAttribute("style") == "") {
-					node.removeAttribute("style");
-				}
-			} else {
-				node.style[propertyName] = propertyValue;
-			}
-
-			// "Let element children be the Element children of node."
-			var elementChildren = [];
-			for (var j = 0; j < node.childNodes.length; j++) {
-				if (node.childNodes[j].nodeType == Node.ELEMENT_NODE) {
-					elementChildren.push(node.childNodes[j]);
-				}
-			}
-
-			// "Unstyle each Element in element children, in order."
-			for (var j = 0; j < elementChildren.length; j++) {
-				unstyleElement(elementChildren[j], propertyName, tagList);
-			}
-		// "Otherwise, if node is a Text node:"
-		} else if (node.nodeType == Node.TEXT_NODE) {
-			var newParent;
-			// "If the previous sibling of node is an HTML element with local
-			// name in tag list with no attributes, let new parent equal the
-			// previous sibling of node."
-			if (node.previousSibling
-			&& node.previousSibling.nodeType == Node.ELEMENT_NODE
-			&& node.previousSibling.namespaceURI == htmlNamespace
-			&& tagList.indexOf(node.previousSibling.tagName.toLowerCase()) != -1
-			&& node.previousSibling.attributes.length == 0) {
-				newParent = node.previousSibling;
-			} else {
-				// "Otherwise, let new parent be a new HTML element with local
-				// name equal to the first string in tag list, with no
-				// attributes, and ownerDocument the same as node. Append new
-				// parent to node's parent as the previous sibling of node."
-				newParent = node.ownerDocument.createElement(tagList[0]);
-				node.parentNode.insertBefore(newParent, node);
-			}
-
-			// "Append node to new parent as its last child."
-			newParent.appendChild(node);
-		}
-		// "Otherwise, do nothing."
-	}
-}
-
-// Note: because browsers are inconsistent about what to return for computed
-// styles for bold, I'm making propertyValue an array in the implementation.
-function unstyleRange(range, propertyName, propertyValue, tagList) {
-	// "Let node list be the result of decomposing range."
-	var nodeList = decomposeRange(range);
-
-	// "For each node in node list, in order:"
-	for (var i = 0; i < nodeList.length; i++) {
-		var node = nodeList[i];
-
-		// "If node is an Element:"
-		if (node.nodeType == Node.ELEMENT_NODE) {
-			// "Let children be the result of unstyling node."
-			var children = unstyleElement(node, propertyName, tagList);
-
-			// "If node no longer has a parent:"
-			if (!node.parentNode) {
-				// "Insert all the Nodes in children into node list immediately
-				// after node, in order."
-				//
-				// splice() would be perfect, but it requires varargs.  :(
-				nodeList = nodeList.slice(0, i + 1)
-					.concat(children)
-					.concat(nodeList.slice(i + 1));
-
-				// "Continue with the next Node in node list, if any."
-				continue;
-			}
-
-			// "If the computed value of property name for node is not property
-			// value, set the CSS property property name of node to property
-			// value."
-			if (propertyValue.indexOf(getComputedStyle(node)[propertyName]) == -1) {
-				node.style[propertyName] = propertyValue[0];
-			}
-
-			// "Let element children be the Element children of node."
-			var elementChildren = [];
-			for (var j = 0; j < node.childNodes.length; j++) {
-				if (node.childNodes[j].nodeType == Node.ELEMENT_NODE) {
-					elementChildren.push(node.childNodes[j]);
-				}
-			}
-
-			// "Unstyle each Element in element children, in order."
-			for (var j = 0; j < elementChildren.length; j++) {
-				unstyleElement(elementChildren[j], propertyName, tagList);
-			}
-		// "Otherwise, if node is a Text node and the computed value of
-		// property name for node's parent is not property value:"
-		} else if (node.nodeType == Node.TEXT_NODE
-		&& propertyValue.indexOf(getComputedStyle(node.parentNode)[propertyName]) == -1) {
-			// "Let new parent be a new HTML element with name "span", with no
-			// attributes, and with ownerDocument equal to node's."
-			var newParent = node.ownerDocument.createElement("span");
-
-			// "Set the CSS property property name of new parent to property
-			// value."
-			newParent.style[propertyName] = propertyValue[0];
-
-			// "Insert new parent as node's previous sibling."
-			node.parentNode.insertBefore(newParent, node);
-
-			// "Append node to new parent as its child."
-			newParent.appendChild(node);
-		}
-		// "Otherwise, do nothing."
-	}
-}
-
-function bold() {
-	var selection = getSelection();
-	for (var i = 0; i < selection.rangeCount; i++) {
-		var fontWeight = getComputedStyle(beginningElement(selection.getRangeAt(i))).fontWeight;
-		if (fontWeight != "bold"
-		&& (!/^[0-9]+$/.test(fontWeight) || fontWeight < 700)) {
-			styleRange(selection.getRangeAt(i), "fontWeight", "bold", ["b", "strong"]);
-		} else {
-			unstyleRange(selection.getRangeAt(i), "fontWeight", ["normal", "400"], ["b", "strong"]);
-		}
-	}
-}
-</script>
--- a/test/editcommands.html	Mon Feb 21 14:25:51 2011 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-<!doctype html>
-<title>execCommand() tests</title>
-<button onclick="document.execCommand('bold', false, null)">Bold</button>
-<div contenteditable=true>
-	<p><b>Abcdef</b>
-	<p><strong>Abcdef</strong>
-	<p><span style=font-weight:bold>Abcdef</span>
-	<p><span style=font-weight:bolder>Abcdef</span>
-	<p><span style=font-weight:900>Abcdef</span>
-</div>
--- a/test/support/diff_match_patch.js	Mon Feb 21 14:25:51 2011 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-// Apache License 2.0, from http://code.google.com/p/google-diff-match-patch/
-(function(){function diff_match_patch(){this.Diff_Timeout=1;this.Diff_EditCost=4;this.Match_Threshold=0.5;this.Match_Distance=1E3;this.Patch_DeleteThreshold=0.5;this.Patch_Margin=4;this.Match_MaxBits=32}
-diff_match_patch.prototype.diff_main=function(a,b,c,d){if(typeof d=="undefined")d=this.Diff_Timeout<=0?Number.MAX_VALUE:(new Date).getTime()+this.Diff_Timeout*1E3;if(a==null||b==null)throw Error("Null input. (diff_main)");if(a==b){if(a)return[[0,a]];return[]}if(typeof c=="undefined")c=true;var e=c,f=this.diff_commonPrefix(a,b);c=a.substring(0,f);a=a.substring(f);b=b.substring(f);f=this.diff_commonSuffix(a,b);var g=a.substring(a.length-f);a=a.substring(0,a.length-f);b=b.substring(0,b.length-f);a=this.diff_compute_(a,
-b,e,d);c&&a.unshift([0,c]);g&&a.push([0,g]);this.diff_cleanupMerge(a);return a};
-diff_match_patch.prototype.diff_compute_=function(a,b,c,d){if(!a)return[[1,b]];if(!b)return[[-1,a]];var e=a.length>b.length?a:b,f=a.length>b.length?b:a,g=e.indexOf(f);if(g!=-1){c=[[1,e.substring(0,g)],[0,f],[1,e.substring(g+f.length)]];if(a.length>b.length)c[0][0]=c[2][0]=-1;return c}if(f.length==1)return[[-1,a],[1,b]];if(e=this.diff_halfMatch_(a,b)){f=e[0];a=e[1];g=e[2];b=e[3];e=e[4];f=this.diff_main(f,g,c,d);c=this.diff_main(a,b,c,d);return f.concat([[0,e]],c)}if(c&&a.length>100&&b.length>100)return this.diff_lineMode_(a,
-b,d);return this.diff_bisect_(a,b,d)};
-diff_match_patch.prototype.diff_lineMode_=function(a,b,c){var d=this.diff_linesToChars_(a,b);a=d[0];b=d[1];d=d[2];a=this.diff_bisect_(a,b,c);this.diff_charsToLines_(a,d);this.diff_cleanupSemantic(a);a.push([0,""]);for(var e=b=0,f=0,g=d="";b<a.length;){switch(a[b][0]){case 1:f++;g+=a[b][1];break;case -1:e++;d+=a[b][1];break;case 0:if(e>=1&&f>=1){d=this.diff_main(d,g,false,c);a.splice(b-e-f,e+f);b=b-e-f;for(e=d.length-1;e>=0;e--)a.splice(b,0,d[e]);b+=d.length}e=f=0;g=d=""}b++}a.pop();return a};
-diff_match_patch.prototype.diff_bisect_=function(a,b,c){for(var d=a.length,e=b.length,f=Math.ceil((d+e)/2),g=f,h=2*f,j=Array(h),i=Array(h),k=0;k<h;k++){j[k]=-1;i[k]=-1}j[g+1]=0;i[g+1]=0;k=d-e;for(var l=k%2!=0,s=0,p=0,q=0,t=0,r=0;r<f;r++){if((new Date).getTime()>c)break;for(var n=-r+s;n<=r-p;n+=2){var m=g+n,o;o=n==-r||n!=r&&j[m-1]<j[m+1]?j[m+1]:j[m-1]+1;for(var u=o-n;o<d&&u<e&&a.charAt(o)==b.charAt(u);){o++;u++}j[m]=o;if(o>d)p+=2;else if(u>e)s+=2;else if(l){m=g+k-n;if(m>=0&&m<h&&i[m]!=-1){var v=d-
-i[m];if(o>=v)return this.diff_bisectSplit_(a,b,o,u,c)}}}for(n=-r+q;n<=r-t;n+=2){m=g+n;v=n==-r||n!=r&&i[m-1]<i[m+1]?i[m+1]:i[m-1]+1;for(o=v-n;v<d&&o<e&&a.charAt(d-v-1)==b.charAt(e-o-1);){v++;o++}i[m]=v;if(v>d)t+=2;else if(o>e)q+=2;else if(!l){m=g+k-n;if(m>=0&&m<h&&j[m]!=-1){o=j[m];u=g+o-m;v=d-v;if(o>=v)return this.diff_bisectSplit_(a,b,o,u,c)}}}}return[[-1,a],[1,b]]};
-diff_match_patch.prototype.diff_bisectSplit_=function(a,b,c,d,e){var f=a.substring(0,c),g=b.substring(0,d);a=a.substring(c);b=b.substring(d);f=this.diff_main(f,g,false,e);e=this.diff_main(a,b,false,e);return f.concat(e)};
-diff_match_patch.prototype.diff_linesToChars_=function(a,b){function c(h){for(var j="",i=0,k=-1,l=d.length;k<h.length-1;){k=h.indexOf("\n",i);if(k==-1)k=h.length-1;var s=h.substring(i,k+1);i=k+1;if(e.hasOwnProperty?e.hasOwnProperty(s):e[s]!==undefined)j+=String.fromCharCode(e[s]);else{j+=String.fromCharCode(l);e[s]=l;d[l++]=s}}return j}var d=[],e={};d[0]="";var f=c(a),g=c(b);return[f,g,d]};
-diff_match_patch.prototype.diff_charsToLines_=function(a,b){for(var c=0;c<a.length;c++){for(var d=a[c][1],e=[],f=0;f<d.length;f++)e[f]=b[d.charCodeAt(f)];a[c][1]=e.join("")}};diff_match_patch.prototype.diff_commonPrefix=function(a,b){if(!a||!b||a.charAt(0)!=b.charAt(0))return 0;for(var c=0,d=Math.min(a.length,b.length),e=d,f=0;c<e;){if(a.substring(f,e)==b.substring(f,e))f=c=e;else d=e;e=Math.floor((d-c)/2+c)}return e};
-diff_match_patch.prototype.diff_commonSuffix=function(a,b){if(!a||!b||a.charAt(a.length-1)!=b.charAt(b.length-1))return 0;for(var c=0,d=Math.min(a.length,b.length),e=d,f=0;c<e;){if(a.substring(a.length-e,a.length-f)==b.substring(b.length-e,b.length-f))f=c=e;else d=e;e=Math.floor((d-c)/2+c)}return e};
-diff_match_patch.prototype.diff_commonOverlap_=function(a,b){var c=a.length,d=b.length;if(c==0||d==0)return 0;if(c>d)a=a.substring(c-d);else if(c<d)b=b.substring(0,c);c=Math.min(c,d);if(a==b)return c;d=0;for(var e=1;;){var f=a.substring(c-e);f=b.indexOf(f);if(f==-1)return d;e+=f;if(f==0||a.substring(c-e)==b.substring(0,e)){d=e;e++}}};
-diff_match_patch.prototype.diff_halfMatch_=function(a,b){function c(i,k,l){for(var s=i.substring(l,l+Math.floor(i.length/4)),p=-1,q="",t,r,n,m;(p=k.indexOf(s,p+1))!=-1;){var o=f.diff_commonPrefix(i.substring(l),k.substring(p)),u=f.diff_commonSuffix(i.substring(0,l),k.substring(0,p));if(q.length<u+o){q=k.substring(p-u,p)+k.substring(p,p+o);t=i.substring(0,l-u);r=i.substring(l+o);n=k.substring(0,p-u);m=k.substring(p+o)}}return q.length*2>=i.length?[t,r,n,m,q]:null}if(this.Diff_Timeout<=0)return null;
-var d=a.length>b.length?a:b,e=a.length>b.length?b:a;if(d.length<4||e.length*2<d.length)return null;var f=this,g=c(d,e,Math.ceil(d.length/4));d=c(d,e,Math.ceil(d.length/2));var h;if(!g&&!d)return null;else h=d?g?g[4].length>d[4].length?g:d:d:g;var j;if(a.length>b.length){g=h[0];d=h[1];e=h[2];j=h[3]}else{e=h[0];j=h[1];g=h[2];d=h[3]}h=h[4];return[g,d,e,j,h]};
-diff_match_patch.prototype.diff_cleanupSemantic=function(a){for(var b=false,c=[],d=0,e=null,f=0,g=0,h=0,j=0,i=0;f<a.length;){if(a[f][0]==0){c[d++]=f;g=j;h=i;i=j=0;e=a[f][1]}else{if(a[f][0]==1)j+=a[f][1].length;else i+=a[f][1].length;if(e!==null&&e.length<=Math.max(g,h)&&e.length<=Math.max(j,i)){a.splice(c[d-1],0,[-1,e]);a[c[d-1]+1][0]=1;d--;d--;f=d>0?c[d-1]:-1;i=j=h=g=0;e=null;b=true}}f++}b&&this.diff_cleanupMerge(a);this.diff_cleanupSemanticLossless(a);for(f=1;f<a.length;){if(a[f-1][0]==-1&&a[f][0]==
-1){b=a[f-1][1];c=a[f][1];if(d=this.diff_commonOverlap_(b,c)){a.splice(f,0,[0,c.substring(0,d)]);a[f-1][1]=b.substring(0,b.length-d);a[f+1][1]=c.substring(d);f++}f++}f++}};
-diff_match_patch.prototype.diff_cleanupSemanticLossless=function(a){function b(r,n){if(!r||!n)return 5;var m=0;if(r.charAt(r.length-1).match(c)||n.charAt(0).match(c)){m++;if(r.charAt(r.length-1).match(d)||n.charAt(0).match(d)){m++;if(r.charAt(r.length-1).match(e)||n.charAt(0).match(e)){m++;if(r.match(f)||n.match(g))m++}}}return m}for(var c=/[^a-zA-Z0-9]/,d=/\s/,e=/[\r\n]/,f=/\n\r?\n$/,g=/^\r?\n\r?\n/,h=1;h<a.length-1;){if(a[h-1][0]==0&&a[h+1][0]==0){var j=a[h-1][1],i=a[h][1],k=a[h+1][1],l=this.diff_commonSuffix(j,
-i);if(l){var s=i.substring(i.length-l);j=j.substring(0,j.length-l);i=s+i.substring(0,i.length-l);k=s+k}l=j;s=i;for(var p=k,q=b(j,i)+b(i,k);i.charAt(0)===k.charAt(0);){j+=i.charAt(0);i=i.substring(1)+k.charAt(0);k=k.substring(1);var t=b(j,i)+b(i,k);if(t>=q){q=t;l=j;s=i;p=k}}if(a[h-1][1]!=l){if(l)a[h-1][1]=l;else{a.splice(h-1,1);h--}a[h][1]=s;if(p)a[h+1][1]=p;else{a.splice(h+1,1);h--}}}h++}};
-diff_match_patch.prototype.diff_cleanupEfficiency=function(a){for(var b=false,c=[],d=0,e="",f=0,g=false,h=false,j=false,i=false;f<a.length;){if(a[f][0]==0){if(a[f][1].length<this.Diff_EditCost&&(j||i)){c[d++]=f;g=j;h=i;e=a[f][1]}else{d=0;e=""}j=i=false}else{if(a[f][0]==-1)i=true;else j=true;if(e&&(g&&h&&j&&i||e.length<this.Diff_EditCost/2&&g+h+j+i==3)){a.splice(c[d-1],0,[-1,e]);a[c[d-1]+1][0]=1;d--;e="";if(g&&h){j=i=true;d=0}else{d--;f=d>0?c[d-1]:-1;j=i=false}b=true}}f++}b&&this.diff_cleanupMerge(a)};
-diff_match_patch.prototype.diff_cleanupMerge=function(a){a.push([0,""]);for(var b=0,c=0,d=0,e="",f="",g;b<a.length;)switch(a[b][0]){case 1:d++;f+=a[b][1];b++;break;case -1:c++;e+=a[b][1];b++;break;case 0:if(c+d>1){if(c!==0&&d!==0){g=this.diff_commonPrefix(f,e);if(g!==0){if(b-c-d>0&&a[b-c-d-1][0]==0)a[b-c-d-1][1]+=f.substring(0,g);else{a.splice(0,0,[0,f.substring(0,g)]);b++}f=f.substring(g);e=e.substring(g)}g=this.diff_commonSuffix(f,e);if(g!==0){a[b][1]=f.substring(f.length-g)+a[b][1];f=f.substring(0,
-f.length-g);e=e.substring(0,e.length-g)}}if(c===0)a.splice(b-c-d,c+d,[1,f]);else d===0?a.splice(b-c-d,c+d,[-1,e]):a.splice(b-c-d,c+d,[-1,e],[1,f]);b=b-c-d+(c?1:0)+(d?1:0)+1}else if(b!==0&&a[b-1][0]==0){a[b-1][1]+=a[b][1];a.splice(b,1)}else b++;c=d=0;f=e=""}a[a.length-1][1]===""&&a.pop();c=false;for(b=1;b<a.length-1;){if(a[b-1][0]==0&&a[b+1][0]==0)if(a[b][1].substring(a[b][1].length-a[b-1][1].length)==a[b-1][1]){a[b][1]=a[b-1][1]+a[b][1].substring(0,a[b][1].length-a[b-1][1].length);a[b+1][1]=a[b-1][1]+
-a[b+1][1];a.splice(b-1,1);c=true}else if(a[b][1].substring(0,a[b+1][1].length)==a[b+1][1]){a[b-1][1]+=a[b+1][1];a[b][1]=a[b][1].substring(a[b+1][1].length)+a[b+1][1];a.splice(b+1,1);c=true}b++}c&&this.diff_cleanupMerge(a)};diff_match_patch.prototype.diff_xIndex=function(a,b){var c=0,d=0,e=0,f=0,g;for(g=0;g<a.length;g++){if(a[g][0]!==1)c+=a[g][1].length;if(a[g][0]!==-1)d+=a[g][1].length;if(c>b)break;e=c;f=d}if(a.length!=g&&a[g][0]===-1)return f;return f+(b-e)};
-diff_match_patch.prototype.diff_prettyHtml=function(a){for(var b=[],c=0,d=/&/g,e=/</g,f=/>/g,g=/\n/g,h=0;h<a.length;h++){var j=a[h][0],i=a[h][1],k=i.replace(d,"&amp;").replace(e,"&lt;").replace(f,"&gt;").replace(g,"&para;<br>");switch(j){case 1:b[h]='<ins style="background:#e6ffe6;">'+k+"</ins>";break;case -1:b[h]='<del style="background:#ffe6e6;">'+k+"</del>";break;case 0:b[h]="<span>"+k+"</span>"}if(j!==-1)c+=i.length}return b.join("")};
-diff_match_patch.prototype.diff_text1=function(a){for(var b=[],c=0;c<a.length;c++)if(a[c][0]!==1)b[c]=a[c][1];return b.join("")};diff_match_patch.prototype.diff_text2=function(a){for(var b=[],c=0;c<a.length;c++)if(a[c][0]!==-1)b[c]=a[c][1];return b.join("")};diff_match_patch.prototype.diff_levenshtein=function(a){for(var b=0,c=0,d=0,e=0;e<a.length;e++){var f=a[e][0],g=a[e][1];switch(f){case 1:c+=g.length;break;case -1:d+=g.length;break;case 0:b+=Math.max(c,d);d=c=0}}b+=Math.max(c,d);return b};
-diff_match_patch.prototype.diff_toDelta=function(a){for(var b=[],c=0;c<a.length;c++)switch(a[c][0]){case 1:b[c]="+"+encodeURI(a[c][1]);break;case -1:b[c]="-"+a[c][1].length;break;case 0:b[c]="="+a[c][1].length}return b.join("\t").replace(/%20/g," ")};
-diff_match_patch.prototype.diff_fromDelta=function(a,b){for(var c=[],d=0,e=0,f=b.split(/\t/g),g=0;g<f.length;g++){var h=f[g].substring(1);switch(f[g].charAt(0)){case "+":try{c[d++]=[1,decodeURI(h)]}catch(j){throw Error("Illegal escape in diff_fromDelta: "+h);}break;case "-":case "=":var i=parseInt(h,10);if(isNaN(i)||i<0)throw Error("Invalid number in diff_fromDelta: "+h);h=a.substring(e,e+=i);if(f[g].charAt(0)=="=")c[d++]=[0,h];else c[d++]=[-1,h];break;default:if(f[g])throw Error("Invalid diff operation in diff_fromDelta: "+
-f[g]);}}if(e!=a.length)throw Error("Delta length ("+e+") does not equal source text length ("+a.length+").");return c};diff_match_patch.prototype.match_main=function(a,b,c){if(a==null||b==null||c==null)throw Error("Null input. (match_main)");c=Math.max(0,Math.min(c,a.length));return a==b?0:a.length?a.substring(c,c+b.length)==b?c:this.match_bitap_(a,b,c):-1};
-diff_match_patch.prototype.match_bitap_=function(a,b,c){function d(r,n){var m=r/b.length,o=Math.abs(c-n);if(!f.Match_Distance)return o?1:m;return m+o/f.Match_Distance}if(b.length>this.Match_MaxBits)throw Error("Pattern too long for this browser.");var e=this.match_alphabet_(b),f=this,g=this.Match_Threshold,h=a.indexOf(b,c);if(h!=-1){g=Math.min(d(0,h),g);h=a.lastIndexOf(b,c+b.length);if(h!=-1)g=Math.min(d(0,h),g)}var j=1<<b.length-1;h=-1;for(var i,k,l=b.length+a.length,s,p=0;p<b.length;p++){i=0;for(k=
-l;i<k;){if(d(p,c+k)<=g)i=k;else l=k;k=Math.floor((l-i)/2+i)}l=k;i=Math.max(1,c-k+1);var q=Math.min(c+k,a.length)+b.length;k=Array(q+2);for(k[q+1]=(1<<p)-1;q>=i;q--){var t=e[a.charAt(q-1)];k[q]=p===0?(k[q+1]<<1|1)&t:(k[q+1]<<1|1)&t|(s[q+1]|s[q])<<1|1|s[q+1];if(k[q]&j){t=d(p,q-1);if(t<=g){g=t;h=q-1;if(h>c)i=Math.max(1,2*c-h);else break}}}if(d(p+1,c)>g)break;s=k}return h};
-diff_match_patch.prototype.match_alphabet_=function(a){for(var b={},c=0;c<a.length;c++)b[a.charAt(c)]=0;for(c=0;c<a.length;c++)b[a.charAt(c)]|=1<<a.length-c-1;return b};
-diff_match_patch.prototype.patch_addContext_=function(a,b){if(b.length!=0){for(var c=b.substring(a.start2,a.start2+a.length1),d=0;b.indexOf(c)!=b.lastIndexOf(c)&&c.length<this.Match_MaxBits-this.Patch_Margin-this.Patch_Margin;){d+=this.Patch_Margin;c=b.substring(a.start2-d,a.start2+a.length1+d)}d+=this.Patch_Margin;(c=b.substring(a.start2-d,a.start2))&&a.diffs.unshift([0,c]);(d=b.substring(a.start2+a.length1,a.start2+a.length1+d))&&a.diffs.push([0,d]);a.start1-=c.length;a.start2-=c.length;a.length1+=
-c.length+d.length;a.length2+=c.length+d.length}};
-diff_match_patch.prototype.patch_make=function(a,b,c){var d;if(typeof a=="string"&&typeof b=="string"&&typeof c=="undefined"){d=a;b=this.diff_main(d,b,true);if(b.length>2){this.diff_cleanupSemantic(b);this.diff_cleanupEfficiency(b)}}else if(a&&typeof a=="object"&&typeof b=="undefined"&&typeof c=="undefined"){b=a;d=this.diff_text1(b)}else if(typeof a=="string"&&b&&typeof b=="object"&&typeof c=="undefined")d=a;else if(typeof a=="string"&&typeof b=="string"&&c&&typeof c=="object"){d=a;b=c}else throw Error("Unknown call format to patch_make.");
-if(b.length===0)return[];c=[];a=new patch_obj;for(var e=0,f=0,g=0,h=d,j=0;j<b.length;j++){var i=b[j][0],k=b[j][1];if(!e&&i!==0){a.start1=f;a.start2=g}switch(i){case 1:a.diffs[e++]=b[j];a.length2+=k.length;d=d.substring(0,g)+k+d.substring(g);break;case -1:a.length1+=k.length;a.diffs[e++]=b[j];d=d.substring(0,g)+d.substring(g+k.length);break;case 0:if(k.length<=2*this.Patch_Margin&&e&&b.length!=j+1){a.diffs[e++]=b[j];a.length1+=k.length;a.length2+=k.length}else if(k.length>=2*this.Patch_Margin)if(e){this.patch_addContext_(a,
-h);c.push(a);a=new patch_obj;e=0;h=d;f=g}}if(i!==1)f+=k.length;if(i!==-1)g+=k.length}if(e){this.patch_addContext_(a,h);c.push(a)}return c};diff_match_patch.prototype.patch_deepCopy=function(a){for(var b=[],c=0;c<a.length;c++){var d=a[c],e=new patch_obj;e.diffs=[];for(var f=0;f<d.diffs.length;f++)e.diffs[f]=d.diffs[f].slice();e.start1=d.start1;e.start2=d.start2;e.length1=d.length1;e.length2=d.length2;b[c]=e}return b};
-diff_match_patch.prototype.patch_apply=function(a,b){if(a.length==0)return[b,[]];a=this.patch_deepCopy(a);var c=this.patch_addPadding(a);b=c+b+c;this.patch_splitMax(a);for(var d=0,e=[],f=0;f<a.length;f++){var g=a[f].start2+d,h=this.diff_text1(a[f].diffs),j,i=-1;if(h.length>this.Match_MaxBits){j=this.match_main(b,h.substring(0,this.Match_MaxBits),g);if(j!=-1){i=this.match_main(b,h.substring(h.length-this.Match_MaxBits),g+h.length-this.Match_MaxBits);if(i==-1||j>=i)j=-1}}else j=this.match_main(b,h,
-g);if(j==-1){e[f]=false;d-=a[f].length2-a[f].length1}else{e[f]=true;d=j-g;g=i==-1?b.substring(j,j+h.length):b.substring(j,i+this.Match_MaxBits);if(h==g)b=b.substring(0,j)+this.diff_text2(a[f].diffs)+b.substring(j+h.length);else{g=this.diff_main(h,g,false);if(h.length>this.Match_MaxBits&&this.diff_levenshtein(g)/h.length>this.Patch_DeleteThreshold)e[f]=false;else{this.diff_cleanupSemanticLossless(g);h=0;var k;for(i=0;i<a[f].diffs.length;i++){var l=a[f].diffs[i];if(l[0]!==0)k=this.diff_xIndex(g,h);
-if(l[0]===1)b=b.substring(0,j+k)+l[1]+b.substring(j+k);else if(l[0]===-1)b=b.substring(0,j+k)+b.substring(j+this.diff_xIndex(g,h+l[1].length));if(l[0]!==-1)h+=l[1].length}}}}}b=b.substring(c.length,b.length-c.length);return[b,e]};
-diff_match_patch.prototype.patch_addPadding=function(a){for(var b=this.Patch_Margin,c="",d=1;d<=b;d++)c+=String.fromCharCode(d);for(d=0;d<a.length;d++){a[d].start1+=b;a[d].start2+=b}d=a[0];var e=d.diffs;if(e.length==0||e[0][0]!=0){e.unshift([0,c]);d.start1-=b;d.start2-=b;d.length1+=b;d.length2+=b}else if(b>e[0][1].length){var f=b-e[0][1].length;e[0][1]=c.substring(e[0][1].length)+e[0][1];d.start1-=f;d.start2-=f;d.length1+=f;d.length2+=f}d=a[a.length-1];e=d.diffs;if(e.length==0||e[e.length-1][0]!=
-0){e.push([0,c]);d.length1+=b;d.length2+=b}else if(b>e[e.length-1][1].length){f=b-e[e.length-1][1].length;e[e.length-1][1]+=c.substring(0,f);d.length1+=f;d.length2+=f}return c};
-diff_match_patch.prototype.patch_splitMax=function(a){for(var b=this.Match_MaxBits,c=0;c<a.length;c++)if(a[c].length1>b){var d=a[c];a.splice(c--,1);for(var e=d.start1,f=d.start2,g="";d.diffs.length!==0;){var h=new patch_obj,j=true;h.start1=e-g.length;h.start2=f-g.length;if(g!==""){h.length1=h.length2=g.length;h.diffs.push([0,g])}for(;d.diffs.length!==0&&h.length1<b-this.Patch_Margin;){g=d.diffs[0][0];var i=d.diffs[0][1];if(g===1){h.length2+=i.length;f+=i.length;h.diffs.push(d.diffs.shift());j=false}else if(g===
--1&&h.diffs.length==1&&h.diffs[0][0]==0&&i.length>2*b){h.length1+=i.length;e+=i.length;j=false;h.diffs.push([g,i]);d.diffs.shift()}else{i=i.substring(0,b-h.length1-this.Patch_Margin);h.length1+=i.length;e+=i.length;if(g===0){h.length2+=i.length;f+=i.length}else j=false;h.diffs.push([g,i]);if(i==d.diffs[0][1])d.diffs.shift();else d.diffs[0][1]=d.diffs[0][1].substring(i.length)}}g=this.diff_text2(h.diffs);g=g.substring(g.length-this.Patch_Margin);i=this.diff_text1(d.diffs).substring(0,this.Patch_Margin);
-if(i!==""){h.length1+=i.length;h.length2+=i.length;if(h.diffs.length!==0&&h.diffs[h.diffs.length-1][0]===0)h.diffs[h.diffs.length-1][1]+=i;else h.diffs.push([0,i])}j||a.splice(++c,0,h)}}};diff_match_patch.prototype.patch_toText=function(a){for(var b=[],c=0;c<a.length;c++)b[c]=a[c];return b.join("")};
-diff_match_patch.prototype.patch_fromText=function(a){var b=[];if(!a)return b;a=a.split("\n");for(var c=0,d=/^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/;c<a.length;){var e=a[c].match(d);if(!e)throw Error("Invalid patch string: "+a[c]);var f=new patch_obj;b.push(f);f.start1=parseInt(e[1],10);if(e[2]===""){f.start1--;f.length1=1}else if(e[2]=="0")f.length1=0;else{f.start1--;f.length1=parseInt(e[2],10)}f.start2=parseInt(e[3],10);if(e[4]===""){f.start2--;f.length2=1}else if(e[4]=="0")f.length2=0;else{f.start2--;
-f.length2=parseInt(e[4],10)}for(c++;c<a.length;){e=a[c].charAt(0);try{var g=decodeURI(a[c].substring(1))}catch(h){throw Error("Illegal escape in patch_fromText: "+g);}if(e=="-")f.diffs.push([-1,g]);else if(e=="+")f.diffs.push([1,g]);else if(e==" ")f.diffs.push([0,g]);else if(e=="@")break;else if(e!=="")throw Error('Invalid patch mode "'+e+'" in: '+g);c++}}return b};function patch_obj(){this.diffs=[];this.start2=this.start1=null;this.length2=this.length1=0}
-patch_obj.prototype.toString=function(){var a,b;a=this.length1===0?this.start1+",0":this.length1==1?this.start1+1:this.start1+1+","+this.length1;b=this.length2===0?this.start2+",0":this.length2==1?this.start2+1:this.start2+1+","+this.length2;a=["@@ -"+a+" +"+b+" @@\n"];var c;for(b=0;b<this.diffs.length;b++){switch(this.diffs[b][0]){case 1:c="+";break;case -1:c="-";break;case 0:c=" "}a[b+1]=c+encodeURI(this.diffs[b][1])+"\n"}return a.join("").replace(/%20/g," ")};window.diff_match_patch=diff_match_patch;
-window.patch_obj=patch_obj;window.DIFF_DELETE=-1;window.DIFF_INSERT=1;window.DIFF_EQUAL=0;})()
--- a/test/support/document.html	Mon Feb 21 14:25:51 2011 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-<!doctype html>
-<title>A document</title>
-<p>This is a document.
--- a/test/support/test.js	Mon Feb 21 14:25:51 2011 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,211 +0,0 @@
-"use strict";
-var Test = {
-  "meta" : { 'helps' : [], 'authors' : [], 'reviewers' : [], 'assert' : "" },
-  "results" : { 'passes' : 0, 'fails' : 0 },
-  "_output" : null,
-
-  "_prepareHeader" : function prepareHeader(aPass) {
-    if (this._output)
-      return true;
-
-    if (!document || !document.body)
-      return false;
-
-    // Prepare output box.
-    this._output = document.body.appendChild(document.createElement("div"));
-    this._output.id = "output";
-
-    var p = document.createElement("p");
-    p.appendChild(document.createTextNode("Passes: "));
-    p.appendChild(document.createTextNode("0"));
-    this._output.appendChild(p);
-    p = document.createElement("p");
-    p.appendChild(document.createTextNode("Fails: "));
-    p.appendChild(document.createTextNode("0"));
-    this._output.appendChild(p);
-    p = document.createElement("p");
-    p.appendChild(document.createTextNode("Score: "));
-    p.appendChild(document.createTextNode("0"));
-    p.appendChild(document.createTextNode("%"));
-    this._output.appendChild(p);
-
-    // Toggling
-    var label = document.createElement("label");
-    label.htmlFor = "toggle";
-    label.appendChild(document.createTextNode("Show/hide details"));
-    var cb = document.createElement("input");
-    cb.type = "checkbox";
-    cb.checked = true;
-    cb.id = "toggle";
-    this._output.appendChild(label);
-    this._output.appendChild(cb);
-
-    try {
-      // Styling
-      // Did you know that this doesn't work in WebKit? It's too hard, apparently.
-      // See also <http://weblogs.mozillazine.org/bz/archives/020267.html>.
-      var style =
-        "#output { white-space: pre-line; background: green; display: table; " +
-        "border: solid black; } " +
-        "#output.fail { background: red; } " +
-        "#output > p { margin: 0 1em; } " +
-        "#output > label { margin-left: 1em; } " +
-        "#output > input { margin-right: 1em; } " +
-        "#output > input ~ p { margin: 0.5em 0; padding: 0 1em; } " +
-        "#output > input:checked ~ p { display: none; } " +
-        "#output > input + p { border-top: thin solid black; " +
-        "padding-top: 0.5em; } "
-      // And this last line doesn't work in IE.
-      document.getElementsByTagName("head")[0]
-              .appendChild(document.createElement("style"))
-              .appendChild(document.createTextNode(style));
-    } catch (e) {
-      document.body
-              .appendChild(document.createElement("p"))
-              .appendChild(document.createTextNode("I hate IE."));
-    }
-
-    this._output.appendChild(document.createElement("p"));
-
-    return true;
-  },
-
-  "_updateHeader" : function updateHeader(aPass) {
-    if (!this._prepareHeader())
-      return;
-
-    if (!aPass)
-      this._output.className = "fail";
-
-    try {
-      this._output.childNodes[0].lastChild.data = this['results']['passes'];
-      this._output.childNodes[1].lastChild.data = this['results']['fails'];
-      this._output.childNodes[2].childNodes[1].data =
-        (100 * this['results']['passes'] /
-          (this['results']['passes'] + this['results']['fails'])).toFixed(2);
-    } catch (e) {
-    }
-  },
-
-  "_log" : function log(aPass, aMessage) {
-    if (parent.report)
-      parent.report(aPass, aMessage);
-
-    ++this['results'][aPass ? 'passes' : 'fails'];
-
-    this._updateHeader(aPass);
-
-    if (this._prepareHeader())
-      this._output.lastChild.appendChild(
-        document.createTextNode((aPass ? "PASS" : ("FAIL: " + aMessage)) + "\n"));
-
-    return aPass;
-  },
-
-  "separate" : function separate() {
-    if (parent !== window && parent.separate)
-      parent.separate();
-
-    if (!this._prepareHeader())
-      return;
-
-    this._output.appendChild(document.createElement("p"));
-  },
-
-  "finish" : function finish() {
-    if (parent !== window && parent.finishTest)
-      parent.finishTest();
-
-    if (!this._prepareHeader())
-      return;
-
-    this._output
-        .appendChild(document.createElement("p"))
-        .appendChild(document.createTextNode("Finished"));
-  },
-
-  "ok" : function ok(aCondition, aError) {
-    return this._log(!!aCondition, aError);
-  },
-
-  "is" : function is(aGot, aExpected, aMsg) {
-    var msg;
-    if (aMsg)
-      msg = aMsg;
-    else if (typeof aExpected === "string")
-      msg = "got \"" + aGot + "\", expected \"" + aExpected + "\".";
-    else
-      msg = "got " + aGot + ", expected " + aExpected + ".";
-    return this.ok(aGot === aExpected, msg);
-  },
-
-  "throws" : function throws(aLambda, aCode, aType) {
-    var type = aType || "DOMException";
-    try {
-      aLambda();
-      Test.fail("This action should have raised an exception.");
-    } catch (e) {
-      Test.ok(window[type], "Need a " + type + " object.") &&
-      Test.ok(e instanceof window[type], "Wrong exception was raised.") &&
-      Test.is(e.code, window[type][aCode]);
-    }
-  },
-
-  "pass" : function pass(aError) {
-    return this.ok(true, aError);
-  },
-
-  "fail" : function fail(aError) {
-    return this.ok(false, aError);
-  },
-
-  "runUntilFinish" : function runUntilFinish(aTests) {
-    for (var i = 0, il = aTests.length; i < il; ++i) {
-      try {
-        aTests.shift()();
-      } catch (e) {
-        if (e instanceof DOMException) {
-          this.fail("DOMException was thrown (code " + e.code + ").");
-        } else {
-          this.fail("Exception was thrown.");
-        }
-      }
-      this.separate();
-    }
-  },
-
-  "run" : function run(aTests) {
-    this.runUntilFinish(aTests);
-    this.finish();
-  }
-};
-
-(function() {
-  try {
-    // Get metadata.
-    var elts;
-    elts = document.querySelectorAll('link[rel="help"]');
-    for (var i = 0, il = elts.length; i < il; ++i) {
-      Test.meta.helps[i] = { 'text' : elts[i].href, 'href' : elts[i].href };
-    }
-    elts = document
-      .querySelectorAll('link[rel~="author"]');
-    for (var i = 0, il = elts.length; i < il; ++i) {
-      Test.meta.authors[i] = { 'text' : elts[i].title, 'href' : elts[i].href };
-    }
-    elts = document
-      .querySelectorAll('link[rel~="reviewer"]');
-    for (var i = 0, il = elts.length; i < il; ++i) {
-      Test.meta.reviewers[i] = { 'text' : elts[i].title, 'href' : elts[i].href };
-    }
-    var elt = document.querySelector('meta[name="assert"]');
-    if (elt)
-      Test.meta.assert = elt.content;
-  } catch (e) {
-    document.body
-            .appendChild(document.createElement("p"))
-            .appendChild(document.createTextNode("I hate IE."));
-  }
-
-  Test._prepareHeader();
-})();
--- a/test/support/testharness.css	Mon Feb 21 14:25:51 2011 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-html {
-    font-family:DejaVu Sans, Bitstream Vera Sans, Arial, Sans;
-}
-
-table#results {
-    border-collapse:collapse;
-}
-
-table#results th {
-    padding:0;
-    padding-bottom:0.5em;
-    border-bottom:medium solid black;
-}
-
-table#results td {
-    padding:1em;
-    padding-bottom:0.5em;
-    border-bottom:thin solid black;
-}
-
-table#results td.pass {
-    color:green;
-}
-
-table#results td.fail {
-    color:red;
-}
-
-table#results td.timeout {
-    color:red;
-}
-
-table#results tr>td:nth-child(1) {
-    font-variant:small-caps;
-}
-
-table#results span {
-    display:block;
-}
-
-table#results span.expected {
-    font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;
-    white-space:pre;
-}
-
-table#results span.actual {
-    font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;
-    white-space:pre;
-}
-
--- a/test/support/testharness.js	Mon Feb 21 14:25:51 2011 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1006 +0,0 @@
-/*
-Distributed under both the W3C Test Suite License [1] and the W3C
-3-clause BSD License [2]. To contribute to a W3C Test Suite, see the
-policies and contribution forms [3].
-
-[1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license
-[2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license
-[3] http://www.w3.org/2004/10/27-testcases
-*/
-
-/*
- * == Introducion ==
- *
- * This file provides a framework for writing testcases. It is intended to
- * provide a convenient API for making common assertions, and to work both
- * for testing synchronous and asynchronous DOM features in a way that
- * promotes clear, robust, tests.
- *
- * == Basic Usage ==
- *
- * To use this file, import the script into the test document:
- * <script src="http://test.w3.org/resources/testharness.js"></script>
- *
- * Within each file one may define one or more tests. Each test is atomic
- * in the sense that a single test has a single result (pass/fail/timeout).
- * Within each test one may have a number of asserts. The test fails at the
- * first failing assert, and the remainder of the test is (typically) not run.
- *
- * If the file containing the tests is a HTML file with an element of id "log"
- * this will be populated with a table containing the test results after all
- * the tests have run.
- *
- * == Synchronous Tests ==
- *
- * To create a sunchronous test use the test() function:
- *
- * test(test_function, name)
- *
- * test_function is a function that contains the code to test. For example a
- * trivial passing test would be:
- *
- * test(function() {assert_true(true)}, "assert_true with true")
- *
- * The function passed in is run in the test() call.
- *
- * == Asynchronous Tests ==
- *
- * Testing asynchronous features is somewhat more complex since the result of
- * a test may depend on one or more events or other callbacks. The API provided
- * for testing these features is indended to be rather low-level but hopefully
- * applicable to many situations.
- *
- * To create a test, one starts by getting a Test object using async_test:
- *
- * var t = async_test("Simple async test")
- *
- * Assertions can be added to the test by calling the step method of the test
- * object with a function containing the test assertions:
- *
- * t.step(function() {assert_true(true)});
- *
- * When all the steps are complete, the done() method must be called:
- *
- * t.done();
- *
- * == Making assertions ==
- *
- * Functions for making assertions start assert_
- * The best way to get a list is to look in this file for functions names
- * matching that pattern. The general signature is
- *
- * assert_something(actual, expected, description)
- *
- * although not all assertions precisely match this pattern e.g. assert_true
- * only takes actual and description as arguments.
- *
- * The description parameter is used to present more useful error messages when
- * a test fails
- *
- * == Generating tests ==
- *
- * There are scenarios in which is is desirable to create a large number of
- * (synchronous) tests that are internally similar but vary in the parameters
- * used. To make this easier, the generate_tests function allows a single
- * function to be called with each set of parameters in a list:
- *
- * generate_tests(test_function, parameter_lists)
- *
- * For example:
- *
- * generate_tests(assert_equals, [
- *     ["Sum one and one", 1+1, 2],
- *     ["Sum one and zero", 1+0, 1]
- *     ])
- *
- * Is equivalent to:
- *
- * test(function() {assert_equals(1+1, 2)}, "Sum one and one")
- * test(function() {assert_equals(1+0, 1)}, "Sum one and zero")
- *
- * Note that the first item in each parameter list corresponds to the name of
- * the test.
- */
-
-(function ()
-{
-    var debug = false;
-    // default timeout is 5 seconds, test can override if needed
-    var default_timeout = 5000;
-
-    // tests either pass, fail or timeout
-    var status =
-    {
-        PASS: 0,
-        FAIL: 1,
-        TIMEOUT: 2
-    };
-    expose(status, 'status');
-
-    /*
-    * API functions
-    */
-
-    var name_counter = 0;
-    function next_default_name()
-    {
-        //Don't use document.title to work around an Opera bug in XHTML documents
-        var prefix = document.getElementsByTagName("title").length > 0 ?
-                         document.getElementsByTagName("title")[0].firstChild.data :
-                         "Untitled";
-        var suffix = name_counter > 0 ? " " + name_counter : "";
-        name_counter++;
-        return prefix + suffix;
-    }
-
-  function test(func, name, properties)
-    {
-        var test_name = name ? name : next_default_name();
-        properties = properties ? properties : {};
-        var test_obj = new Test(test_name, properties);
-        test_obj.step(func);
-        if (test_obj.status === null) {
-            test_obj.done();
-        }
-    }
-
-    function async_test(name, properties)
-    {
-        var test_name = name ? name : next_default_name();
-        properties = properties ? properties : {};
-        var test_obj = new Test(test_name, properties);
-        return test_obj;
-    }
-
-    function generate_tests(func, args) {
-        forEach(args, function(x)
-                {
-                    var name = x[0];
-                    test(function()
-                         {
-                             func.apply(this, x.slice(1));
-                         }, name);
-                });
-    }
-
-    function on_event(object, event, callback)
-    {
-      object.addEventListener(event, callback, false);
-    }
-
-    expose(test, 'test');
-    expose(async_test, 'async_test');
-    expose(generate_tests, 'generate_tests');
-    expose(on_event, 'on_event');
-
-    /*
-    * Assertions
-    */
-
-    function assert_true(actual, description)
-    {
-        var message = make_message("assert_true", description,
-                                   "expected true got ${actual}", {actual:actual});
-        assert(actual === true, message);
-    };
-    expose(assert_true, "assert_true");
-
-    function assert_false(actual, description)
-    {
-        var message = make_message("assert_false", description,
-                                   "expected false got ${actual}", {actual:actual});
-        assert(actual === false, message);
-    };
-    expose(assert_false, "assert_false");
-
-    function assert_equals(actual, expected, description)
-    {
-         /*
-          * Test if two primitives are equal or two objects
-          * are the same object
-          */
-         var message = make_message("assert_equals", description,
-                                    [["{text}", "expected "],
-                                     ["span", {"class":"expected"}, String(expected)],
-                                     ["{text}", "got "],
-                                     ["span", {"class":"actual"}, String(actual)]]);
-         if (expected !== expected)
-         {
-             //NaN case
-             assert(actual !== actual, message);
-         }
-         else
-         {
-             //typical case
-             assert(actual === expected, message);
-         }
-    };
-    expose(assert_equals, "assert_equals");
-
-    function assert_object_equals(actual, expected, description)
-    {
-         //This needs to be improved a great deal
-         function check_equal(expected, actual, stack)
-         {
-             stack.push(actual);
-
-             for (p in actual)
-             {
-                 var message = make_message(
-                     "assert_object_equals", description,
-                     "unexpected property ${p}", {p:p});
-
-                 assert(expected.hasOwnProperty(p), message);
-
-                 if (typeof actual[p] === "object" && actual[p] !== null)
-                 {
-                     if (stack.indexOf(actual[p]) === -1)
-                     {
-                         check_equal(actual[p], expected[p], stack);
-                     }
-                 }
-                 else
-                 {
-                     message = make_message(
-                         "assert_object_equals", description,
-                         "property ${p} expected ${expected} got ${actual}",
-                         {p:p, expected:expected, actual:actual});
-
-                     assert(actual[p] === expected[p], message);
-                 }
-             }
-             for (p in expected)
-             {
-                 var message = make_message(
-                     "assert_object_equals", description,
-                     "expected property ${p} missing", {p:p});
-
-                 assert(actual.hasOwnProperty(p), message);
-             }
-             stack.pop();
-         }
-         check_equal(actual, expected, []);
-    };
-    expose(assert_object_equals, "assert_object_equals");
-
-    function assert_array_equals(actual, expected, description)
-    {
-        var message = make_message(
-            "assert_array_equals", description,
-            "lengths differ, expected ${expected} got ${actual}",
-            {expected:expected.length, actual:actual.length});
-
-        assert(actual.length === expected.length, message);
-
-        for (var i=0; i < actual.length; i++)
-        {
-            message = make_message(
-                "assert_array_equals", description,
-                "property ${i}, property expected to be $expected but was $actual",
-                {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing",
-                 actual:actual.hasOwnProperty(i) ? "present" : "missing"});
-            assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i), message);
-            message = make_message(
-                          "assert_array_equals", description,
-                          "property ${i}, expected ${expected} but got ${actual}",
-                          {i:i, expected:expected[i], actual:actual[i]});
-            assert(expected[i] === actual[i], message);
-        }
-    }
-    expose(assert_array_equals, "assert_array_equals");
-
-    function assert_array_equals_unsorted(actual, expected, description)
-    {
-        assert_array_equals(actual.sort(), expected.sort(), description);
-    }
-    expose(assert_array_equals_unsorted, "assert_array_equals_unsorted");
-
-    function assert_exists(object, property_name, description)
-    {
-         var message = make_message(
-             "assert_exists", description,
-             "expected property ${p} missing", {p:property_name});
-
-         assert(object.hasOwnProperty(property_name), message);
-    };
-    expose(assert_exists, "assert_exists");
-
-    function assert_not_exists(object, property_name, description)
-    {
-         var message = make_message(
-             "assert_not_exists", description,
-             "unexpected property ${p} found", {p:property_name});
-
-         assert(!object.hasOwnProperty(property_name), message);
-    };
-    expose(assert_not_exists, "assert_not_exists");
-
-    function assert_readonly(object, property_name, description)
-    {
-         var initial_value = object[property_name];
-         try {
-             var message = make_message(
-                 "assert_readonly", description,
-                 "deleting property ${p} succeeded", {p:property_name});
-             assert(delete object[property_name] === false, message);
-             assert(object[property_name] === initial_value, message);
-             //Note that this can have side effects in the case where
-             //the property has PutForwards
-             object[property_name] = initial_value + "a"; //XXX use some other value here?
-             message = make_message("assert_readonly", description,
-                                    "changing property ${p} succeeded",
-                                    {p:property_name});
-             assert(object[property_name] === initial_value, message);
-         }
-         finally
-         {
-             object[property_name] = initial_value;
-         }
-    };
-    expose(assert_readonly, "assert_readonly");
-
-    function assert_throws(code_or_object, func, description)
-    {
-        try
-        {
-            func.call(this);
-            assert(false, make_message("assert_throws", description,
-                                      "${func} did not throw", {func:String(func)}));
-        }
-        catch(e)
-        {
-            if (e instanceof AssertionError) {
-                throw(e);
-            }
-            if (typeof code_or_object === "string")
-            {
-                assert(e[code_or_object] !== undefined &&
-                       e.code === e[code_or_object] &&
-                       e.name === code_or_object,
-                       make_message("assert_throws", description,
-                           [["{text}", "${func} threw with"] ,
-                            function()
-                            {
-                                var actual_name;
-                                for (var p in DOMException)
-                                {
-                                    if (e.code === DOMException[p])
-                                    {
-                                        actual_name = p;
-                                        break;
-                                    }
-                                }
-                                if (actual_name)
-                                {
-                                    return ["{text}", " code " + actual_name + " (${actual_number})"];
-                                }
-                                else
-                                {
-                                    return ["{text}", " error number ${actual_number}"];
-                                }
-                            },
-                            ["{text}"," expected ${expected}"],
-                            function()
-                            {
-                                return e[code_or_object] ?
-                                    ["{text}", " (${expected_number})"] : null;
-                            }
-                           ],
-                                    {func:String(func), actual_number:e.code,
-                                     expected:String(code_or_object),
-                                     expected_number:e[code_or_object]}));
-                assert(e instanceof DOMException,
-                      make_message("assert_throws", description,
-                                   "thrown exception ${exception} was not a DOMException",
-                                  {exception:String(e)}));
-            }
-            else
-            {
-                assert(e instanceof Object && "name" in e && e.name == code_or_object.name,
-                       make_message("assert_throws", description,
-                           "${func} threw ${actual} (${actual_name}) expected ${expected} (${expected_name})",
-                                    {func:String(func), actual:String(e), actual_name:e.name,
-                                     expected:String(code_or_object),
-                                     expected_name:code_or_object.name}));
-            }
-        }
-    }
-    expose(assert_throws, "assert_throws");
-
-    function assert_unreached(description) {
-         var message = make_message("assert_unreached", description,
-                                    "Reached unreachable code");
-
-         assert(false, message);
-    }
-    expose(assert_unreached, "assert_unreached");
-
-    function Test(name, properties)
-    {
-       this.name = name;
-       this.status = null;
-       var timeout = default_timeout;
-       this.is_done = false;
-
-       if (properties.timeout)
-       {
-           timeout = properties.timeout;
-       }
-
-       this.message = null;
-
-       var this_obj = this;
-       this.steps = [];
-       this.timeout_id = setTimeout(function() { this_obj.timeout(); }, timeout);
-
-       tests.push(this);
-   }
-
-    Test.prototype.step = function(func, this_obj)
-    {
-        //In case the test has already failed
-        if (this.status !== null)
-        {
-          return;
-        }
-
-        this.steps.push(func);
-
-        try
-        {
-            func.apply(this_obj);
-        }
-        catch(e)
-        {
-            //This can happen if something called synchronously invoked another
-            //step
-            if (this.status !== null)
-            {
-                return;
-            }
-            this.status = status.FAIL;
-            this.message = e.message;
-            this.done();
-            if (debug) {
-                throw e;
-            }
-        }
-    };
-
-    Test.prototype.timeout = function()
-    {
-        this.status = status.TIMEOUT;
-        this.timeout_id = null;
-        this.message = "Test timed out";
-        this.done();
-    };
-
-    Test.prototype.done = function()
-    {
-        if (this.is_done) {
-            //Using alert here is bad
-            return;
-        }
-        clearTimeout(this.timeout_id);
-        if (this.status == null)
-        {
-            this.status = status.PASS;
-        }
-        this.is_done = true;
-        tests.done(this);
-    };
-
-
-   /*
-    * Harness
-    */
-    var tests = new Tests();
-
-    function Tests()
-    {
-        this.tests = [];
-        this.num_pending = 0;
-        this.started = false;
-
-        this.start_callbacks = [];
-        this.test_done_callbacks = [];
-        this.all_done_callbacks = [];
-
-        var this_obj = this;
-
-        //All tests can't be done until the load event fires
-        this.all_loaded = false;
-
-        on_event(window, "load",
-                 function()
-                 {
-                     this_obj.all_loaded = true;
-                     if (document.getElementById("log"))
-                     {
-                         add_completion_callback(output_results);
-                     }
-                     if (this_obj.all_done())
-                     {
-                         this_obj.notify_results();
-                     }
-                 });
-   }
-
-    Tests.prototype.push = function(test)
-    {
-        if (!this.started) {
-            this.start();
-        }
-        this.num_pending++;
-        this.tests.push(test);
-    };
-
-    Tests.prototype.all_done = function() {
-        return this.all_loaded && this.num_pending == 0;
-    };
-
-    Tests.prototype.done = function(test)
-    {
-        this.num_pending--;
-        var this_obj = this;
-        forEach(this.test_done_callbacks,
-                function(callback)
-                {
-                    callback(test, this_obj);
-                });
-
-        if(top !== window && top.result_callback)
-        {
-            top.result_callback.call(test, this_obj);
-        }
-
-        if (this.all_done())
-        {
-            this.notify_results();
-        }
-
-    };
-
-    Tests.prototype.start = function() {
-        this.started = true;
-        var this_obj = this;
-        forEach (this.start_callbacks,
-                 function(callback)
-                 {
-                     callback(this_obj);
-                 });
-        if(top !== window && top.start_callback)
-        {
-            top.start_callback.call(this_obj);
-        }
-    };
-
-    Tests.prototype.notify_results = function()
-    {
-        var this_obj = this;
-
-        forEach (this.all_done_callbacks,
-                 function(callback)
-                 {
-                     callback(this_obj.tests);
-                 });
-        if(top !== window && top.completion_callback)
-        {
-            top.completion_callback.call(this_obj, this_obj.tests);
-        }
-    };
-
-    function add_start_callback(callback) {
-        tests.start_callbacks.push(callback);
-    }
-
-    function add_result_callback(callback)
-    {
-        tests.test_done_callbacks.push(callback);
-    }
-
-    function add_completion_callback(callback)
-    {
-       tests.all_done_callbacks.push(callback);
-    }
-
-    expose(add_start_callback, 'add_start_callback');
-    expose(add_result_callback, 'add_result_callback');
-    expose(add_completion_callback, 'add_completion_callback');
-
-    /*
-     * Output listener
-    */
-
-    (function show_status() {
-        var done_count = 0;
-         function on_done(test, tests) {
-             var log = document.getElementById("log");
-             done_count++;
-             if (log)
-             {
-                 if (log.lastChild) {
-                     log.removeChild(log.lastChild);
-                 }
-                 var nodes = render([["{text}", "Running, ${done} complete"],
-                                 function() {
-                                     if (tests.all_done) {
-                                         return ["{text}", " ${pending} remain"];
-                                     } else {
-                                         return null;
-                                     }
-                                 }
-                                    ], {done:done_count,
-                                        pending:tests.num_pending});
-                 forEach(nodes, function(node) {
-                             log.appendChild(node);
-                         });
-                 log.normalize();
-             }
-         }
-         if (document.getElementById("log"))
-         {
-             add_result_callback(on_done);
-         }
-     })();
-
-    function output_results(tests)
-    {
-        var log = document.getElementById("log");
-        while (log.lastChild) {
-            log.removeChild(log.lastChild);
-        }
-        var prefix = null;
-        var scripts = document.getElementsByTagName("script");
-        for (var i=0; i<scripts.length; i++)
-        {
-            var src = scripts[i].src;
-            if (src.slice(src.length - "testharness.js".length) === "testharness.js")
-            {
-                prefix = src.slice(0, src.length - "testharness.js".length);
-                break;
-            }
-        }
-        if (prefix != null) {
-            var stylesheet = document.createElement("link");
-            stylesheet.setAttribute("rel", "stylesheet");
-            stylesheet.setAttribute("href", prefix + "testharness.css");
-            var heads = document.getElementsByTagName("head");
-            if (heads) {
-                heads[0].appendChild(stylesheet);
-            }
-        }
-
-        var status_text = {};
-        status_text[status.PASS] = "Pass";
-        status_text[status.FAIL] = "Fail";
-        status_text[status.TIMEOUT] = "Timeout";
-
-        var template = ["table", {"id":"results"},
-                        ["tr", {},
-                         ["th", {}, "Result"],
-                         ["th", {}, "Test Name"],
-                         ["th", {}, "Message"]
-                        ],
-                        function(vars) {
-                            var rv = map(vars.tests, function(test) {
-                                             var status = status_text[test.status];
-                                             return  ["tr", {},
-                                                      ["td", {"class":status.toLowerCase()}, status],
-                                                      ["td", {}, test.name],
-                                                      ["td", {}, test.message ? test.message : " "]
-                                                     ];
-                                         });
-                            return rv;
-                        }
-                       ];
-
-        log.appendChild(render(template, {tests:tests}));
-
-    }
-
-
-    /*
-     * Template code
-     *
-     * A template is just a javascript structure. An element is represented as:
-     *
-     * [tag_name, {attr_name:attr_value}, child1, child2]
-     *
-     * the children can either be strings (which act like text nodes), other templates or
-     * functions (see below)
-     *
-     * A text node is represented as
-     *
-     * ["{text}", value]
-     *
-     * String values have a simple substitution syntax; ${foo} represents a variable foo.
-     *
-     * It is possible to embed logic in templates by using a function in a place where a
-     * node would usually go. The function must either return part of a template or null.
-     *
-     * In cases where a set of nodes are required as output rather than a single node
-     * with children it is possible to just use a list
-     * [node1, node2, node3]
-     *
-     * Usage:
-     *
-     * render(template, substitutions) - take a template and an object mapping
-     * variable names to parameters and return either a DOM node or a list of DOM nodes
-     *
-     * substitute(template, substitutions) - take a template and variable mapping object,
-     * make the variable substitutions and return the substituted template
-     *
-     */
-
-    function is_single_node(template)
-    {
-        return typeof template[0] === "string";
-    }
-
-    function substitute(template, substitutions)
-    {
-        if (typeof template === "function") {
-            var replacement = template(substitutions);
-            if (replacement)
-            {
-                var rv = substitute(replacement, substitutions);
-                return rv;
-            }
-            else
-            {
-                return null;
-            }
-        }
-        else if (is_single_node(template))
-        {
-            return substitute_single(template, substitutions);
-        }
-        else
-        {
-            return filter(map(template, function(x) {
-                                  return substitute(x, substitutions);
-                              }), function(x) {return x !== null;});
-        }
-    }
-    expose(substitute, "template.substitute");
-
-    function substitute_single(template, substitutions)
-    {
-        var substitution_re = /\${([^ }]*)}/g;
-
-        function do_substitution(input) {
-            var components = input.split(substitution_re);
-            var rv = [];
-            for (var i=0; i<components.length; i+=2)
-            {
-                rv.push(components[i]);
-                if (components[i+1])
-                {
-                    rv.push(substitutions[components[i+1]]);
-                }
-            }
-            return rv;
-        }
-
-        var rv = [];
-        rv.push(do_substitution(String(template[0])).join(""));
-
-        if (template[0] === "{text}") {
-            substitute_children(template.slice(1), rv);
-        } else {
-            substitute_attrs(template[1], rv);
-            substitute_children(template.slice(2), rv);
-        }
-
-        function substitute_attrs(attrs, rv)
-        {
-            rv[1] = {};
-            for (name in template[1])
-            {
-                if (attrs.hasOwnProperty(name))
-                {
-                    var new_name = do_substitution(name).join("");
-                    var new_value = do_substitution(attrs[name]).join("");
-                    rv[1][new_name] = new_value;
-                };
-            }
-        }
-
-        function substitute_children(children, rv)
-        {
-            for (var i=0; i<children.length; i++)
-            {
-                if (children[i] instanceof Object) {
-                    var replacement = substitute(children[i], substitutions);
-                    if (replacement !== null)
-                    {
-                        if (is_single_node(replacement))
-                        {
-                            rv.push(replacement);
-                        }
-                        else
-                        {
-                            extend(rv, replacement);
-                        }
-                    }
-                }
-                else
-                {
-                    extend(rv, do_substitution(String(children[i])));
-                }
-            }
-            return rv;
-        }
-
-        return rv;
-    }
-
-    function make_dom_single(template)
-    {
-        if (template[0] === "{text}")
-        {
-            var element = document.createTextNode("");
-            for (var i=1; i<template.length; i++)
-            {
-                element.data += template[i];
-            }
-        }
-        else
-        {
-            var element = document.createElement(template[0]);
-            for (name in template[1]) {
-                if (template[1].hasOwnProperty(name))
-                {
-                    element.setAttribute(name, template[1][name]);
-                }
-            }
-            for (var i=2; i<template.length; i++)
-            {
-                if (template[i] instanceof Object)
-                {
-                    var sub_element = make_dom(template[i]);
-                    element.appendChild(sub_element);
-                }
-                else
-                {
-                    var text_node = document.createTextNode(template[i]);
-                    element.appendChild(text_node);
-                }
-            }
-        }
-
-        return element;
-    }
-
-
-
-    function make_dom(template, substitutions)
-    {
-        if (is_single_node(template))
-        {
-            return make_dom_single(template);
-        }
-        else
-        {
-            return map(template, function(x) {
-                           return make_dom_single(x);
-                       });
-        }
-    }
-
-    function render(template, substitutions)
-    {
-        return make_dom(substitute(template, substitutions));
-    }
-    expose(render, "template.render");
-
-    /*
-     * Utility funcions
-     */
-    function assert(expected_true, message)
-    {
-        if (expected_true !== true)
-        {
-            throw new AssertionError(message);
-        }
-    }
-
-    function AssertionError(message)
-    {
-        this.message = message;
-    }
-
-    function make_message(function_name, description, error, substitutions)
-    {
-        var message = substitute([["span", {"class":"assert"}, "${function_name}:"],
-                                  function()
-                                  {
-                                      if (description) {
-                                          return ["span", {"class":"description"}, description];
-                                      } else {
-                                          return null;
-                                      }
-                                  },
-                                  ["div", {"class":"error"}, error]
-                                 ], merge({function_name:function_name},
-                                         substitutions));
-
-        return message;
-    }
-
-    function filter(array, callable, thisObj) {
-        var rv = [];
-        for (var i=0; i<array.length; i++)
-        {
-            if (array.hasOwnProperty(i))
-            {
-                var pass = callable.call(thisObj, array[i], i, array);
-                if (pass) {
-                    rv.push(array[i]);
-                }
-            }
-        }
-        return rv;
-    }
-
-    function map(array, callable, thisObj)
-    {
-        var rv = [];
-        rv.length = array.length;
-        for (var i=0; i<array.length; i++)
-        {
-            if (array.hasOwnProperty(i))
-            {
-                rv[i] = callable.call(thisObj, array[i], i, array);
-            }
-        }
-        return rv;
-    }
-
-    function extend(array, items)
-    {
-        Array.prototype.push.apply(array, items);
-    }
-
-    function forEach (array, callback, thisObj)
-    {
-        for (var i=0; i<array.length; i++)
-        {
-            if (array.hasOwnProperty(i))
-            {
-                callback.call(thisObj, array[i], i, array);
-            }
-        }
-    }
-
-    function merge(a,b)
-    {
-        var rv = {};
-        var p;
-        for (p in a)
-        {
-            rv[p] = a[p];
-        }
-        for (p in b) {
-            rv[p] = b[p];
-        }
-        return rv;
-    }
-
-    function expose(object, name)
-    {
-        var components = name.split(".");
-        var target = window;
-        for (var i=0; i<components.length - 1; i++)
-        {
-            if (!(components[i] in target))
-            {
-                target[components[i]] = {};
-            }
-            target = target[components[i]];
-        }
-        target[components[components.length - 1]] = object;
-    }
-
-})();
--- a/xrefs.json	Mon Feb 21 14:25:51 2011 -0700
+++ b/xrefs.json	Mon Feb 21 14:54:12 2011 -0700
@@ -1,6 +1,8 @@
 {
   "beginning element": "beginning-element",
   "command-bold": "command-bold",
+  "command-italic": "command-italic",
+  "command-underline": "command-underline",
   "decompose a range": "decompose-a-range",
   "execcommand()": "execcommand()",
   "first node": "first-node",