Preliminary removeFormat spec
authorAryeh Gregor <AryehGregor+gitcommit@gmail.com>
Tue, 12 Apr 2011 14:55:38 -0600
changeset 54 b9b2593b33a9
parent 53 9ca0b4ae819d
child 55 cdfb46b20f06
Preliminary removeFormat spec

Still needs refinement.
autoimplementation.html
editcommands.html
implementation.js
source.html
--- a/autoimplementation.html	Mon Apr 11 14:48:46 2011 -0600
+++ b/autoimplementation.html	Tue Apr 12 14:55:38 2011 -0600
@@ -25,6 +25,7 @@
 dfn { font-style: italic }
 /* We don't want test cells to not wrap */
 listing, plaintext, pre, xmp { white-space: pre-wrap }
+video { width: 50px }
 </style>
 <p>Legend: {[ are the selection anchor, }] are the selection focus, {}
 represent an element boundary point, [] represent a text node boundary point.
@@ -50,6 +51,7 @@
 	<li><a href=#forecolor>forecolor</a>
 	<li><a href=#hilitecolor>hilitecolor</a>
 	<li><a href=#italic>italic</a>
+	<li><a href=#removeformat>removeformat</a>
 	<li><a href=#strikethrough>strikethrough</a>
 	<li><a href=#subscript>subscript</a>
 	<li><a href=#superscript>superscript</a>
@@ -160,6 +162,17 @@
 <button onclick="addTest('italic')">Add test</button>
 </div>
 
+<div id=removeformat>
+<h1>removeformat</h1>
+
+<button onclick="runTests('removeformat')">Run tests</button>
+
+<table border=1><tr><th>Input <th>Spec <th>Browser <th>Same?</table>
+
+<p><label>New test input: <input></label>
+<button onclick="addTest('removeformat')">Add test</button>
+</div>
+
 <div id=strikethrough>
 <h1>strikethrough</h1>
 
@@ -496,6 +509,11 @@
 		// Minor algorithm bug: this changes the size of the "b" and "r" in
 		// "bar" when we pull down styles
 		["3", '<font size=6>foo <span style="font-size: 2em">b[a]r</span> baz</font>'],
+
+		["3", 'foo<big>[bar]</big>baz'],
+		["3", 'foo<big>b[a]r</big>baz'],
+		["3", 'foo<small>[bar]</small>baz'],
+		["3", 'foo<small>b[a]r</small>baz'],
 	],
 	forecolor: [
 		'foo[bar]baz',
@@ -613,6 +631,115 @@
 		'foo [bar <i>baz] qoz</i> quz sic',
 		'foo bar <i>baz [qoz</i> quz] sic',
 	],
+	removeformat: [
+		'[foo<b>bar</b>baz]',
+		'foo[<b>bar</b>baz]',
+		'foo[<b>bar</b>]baz',
+		'foo<b>[bar]</b>baz',
+		'foo<b>b[a]r</b>baz',
+		'[foo<strong>bar</strong>baz]',
+		'[foo<span style="font-weight: bold">bar</span>baz]',
+		'foo<span style="font-weight: bold">b[a]r</span>baz',
+		'[foo<b id=foo>bar</b>baz]',
+		'foo<b id=foo>b[a]r</b>baz',
+
+		// HTML has lots of inline elements, doesn't it?
+		'[foo<a>bar</a>baz]',
+		'foo<a>b[a]r</a>baz',
+		'[foo<a href=foo>bar</a>baz]',
+		'foo<a href=foo>b[a]r</a>baz',
+		'[foo<abbr>bar</abbr>baz]',
+		'foo<abbr>b[a]r</abbr>baz',
+		'[foo<acronym>bar</acronym>baz]',
+		'foo<acronym>b[a]r</acronym>baz',
+		'[foo<b>bar</b>baz]',
+		'foo<b>b[a]r</b>baz',
+		'[foo<bdi dir=rtl>bar</bdi>baz]',
+		'foo<bdi dir=rtl>b[a]r</bdi>baz',
+		'[foo<bdo dir=rtl>bar</bdo>baz]',
+		'foo<bdo dir=rtl>b[a]r</bdo>baz',
+		'[foo<big>bar</big>baz]',
+		'foo<big>b[a]r</big>baz',
+		'[foo<blink>bar</blink>baz]',
+		'foo<blink>b[a]r</blink>baz',
+		'[foo<cite>bar</cite>baz]',
+		'foo<cite>b[a]r</cite>baz',
+		'[foo<code>bar</code>baz]',
+		'foo<code>b[a]r</code>baz',
+		'[foo<del>bar</del>baz]',
+		'foo<del>b[a]r</del>baz',
+		'[foo<dfn>bar</dfn>baz]',
+		'foo<dfn>b[a]r</dfn>baz',
+		'[foo<em>bar</em>baz]',
+		'foo<em>b[a]r</em>baz',
+		'[foo<font>bar</font>baz]',
+		'foo<font>b[a]r</font>baz',
+		'[foo<font color=red>bar</font>baz]',
+		'foo<font color=red>b[a]r</font>baz',
+		'[foo<i>bar</i>baz]',
+		'foo<i>b[a]r</i>baz',
+		'[foo<ins>bar</ins>baz]',
+		'foo<ins>b[a]r</ins>baz',
+		'[foo<kbd>bar</kbd>baz]',
+		'foo<kbd>b[a]r</kbd>baz',
+		'[foo<mark>bar</mark>baz]',
+		'foo<mark>b[a]r</mark>baz',
+		'[foo<nobr>bar</nobr>baz]',
+		'foo<nobr>b[a]r</nobr>baz',
+		'[foo<q>bar</q>baz]',
+		'foo<q>b[a]r</q>baz',
+		'[foo<samp>bar</samp>baz]',
+		'foo<samp>b[a]r</samp>baz',
+		'[foo<s>bar</s>baz]',
+		'foo<s>b[a]r</s>baz',
+		'[foo<small>bar</small>baz]',
+		'foo<small>b[a]r</small>baz',
+		'[foo<span>bar</span>baz]',
+		'foo<span>b[a]r</span>baz',
+		'[foo<strike>bar</strike>baz]',
+		'foo<strike>b[a]r</strike>baz',
+		'[foo<strong>bar</strong>baz]',
+		'foo<strong>b[a]r</strong>baz',
+		'[foo<sub>bar</sub>baz]',
+		'foo<sub>b[a]r</sub>baz',
+		'[foo<sup>bar</sup>baz]',
+		'foo<sup>b[a]r</sup>baz',
+		'[foo<tt>bar</tt>baz]',
+		'foo<tt>b[a]r</tt>baz',
+		'[foo<u>bar</u>baz]',
+		'foo<u>b[a]r</u>baz',
+		'[foo<var>bar</var>baz]',
+		'foo<var>b[a]r</var>baz',
+
+		// Empty and replaced elements
+		'[foo<br>bar]',
+		'[foo<hr>bar]',
+		'[foo<wbr>bar]',
+		'[foo<img>bar]',
+		'[foo<img src=abc>bar]',
+		'[foo<video></video>bar]',
+		'[foo<video src=abc></video>bar]',
+		'[foo<svg><circle fill=red r=20 cx=20 cy=20 /></svg>bar]',
+
+		// Unrecognized elements
+		'[foo<nonexistentelement>bar</nonexistentelement>baz]',
+		'foo<nonexistentelement>b[a]r</nonexistentelement>baz',
+		'[foo<nonexistentelement style="display: block">bar</nonexistentelement>baz]',
+		'foo<nonexistentelement style="display: block">b[a]r</nonexistentelement>baz',
+
+		'[foo<span id=foo>bar</span>baz]',
+		'foo<span id=foo>b[a]r</span>baz',
+		'[foo<span class=foo>bar</span>baz]',
+		'foo<span class=foo>b[a]r</span>baz',
+		'[foo<b style="font-weight: normal">bar</b>baz]',
+		'foo<b style="font-weight: normal">b[a]r</b>baz',
+		'<p style="background-color: red">foo[bar]baz</p>',
+		'<p><span style="background-color: red">foo[bar]baz</span></p>',
+		'<p style="font-weight: bold">foo[bar]baz</p>',
+		'<b><p style="font-weight: bold">foo[bar]baz</p></b>',
+		'<p style="font-variant: small-caps">foo[bar]baz</p>',
+		'<p style="text-indent: 2em">foo[bar]baz</p>',
+	],
 	strikethrough: [
 		'foo[bar]baz',
 		'foo]bar[baz',
@@ -944,7 +1071,7 @@
 	}
 
 	var key = "execcommand-" + command
-		+ "-" + Number(myQueryCommandState("styleWithCSS"))
+		+ "-" + Number(styleWithCss)
 		+ "-" + tr.firstChild.lastChild.textContent;
 
 	var oldValue = localStorage[key];
@@ -980,6 +1107,10 @@
 	tr.appendChild(browserCell);
 	try {
 		var points = setupCell(browserCell, test);
+		// Work around weird Firefox bug:
+		// https://bugzilla.mozilla.org/show_bug.cgi?id=649138
+		var testDiv = browserCell.firstChild;
+		document.getElementById(command).appendChild(testDiv);
 		setSelection(points[0], points[1], points[2], points[3]);
 		try {
 			document.execCommand("styleWithCSS", false, styleWithCss);
@@ -988,9 +1119,13 @@
 		if (getSelection().rangeCount) {
 			addBrackets(getSelection().getRangeAt(0));
 		}
+		browserCell.insertBefore(testDiv, browserCell.firstChild);
 		browserCell.lastChild.textContent = browserCell.firstChild.innerHTML;
 	} catch (e) {
 		browserCell.textContent = "Exception: " + e;
+		if (testDiv && testDiv.parentNode) {
+			testDiv.parentNode.removeChild(testDiv);
+		}
 	}
 }
 
--- a/editcommands.html	Mon Apr 11 14:48:46 2011 -0600
+++ b/editcommands.html	Tue Apr 12 14:55:38 2011 -0600
@@ -27,7 +27,7 @@
 <body class=draft>
 <div class=head id=head>
 <h1>HTML Editing Commands</h1>
-<h2 class="no-num no-toc" id=work-in-progress-&mdash;-last-update-11-april-2011>Work in Progress &mdash; Last Update 11 April 2011</h2>
+<h2 class="no-num no-toc" id=work-in-progress-&mdash;-last-update-12-april-2011>Work in Progress &mdash; Last Update 12 April 2011</h2>
 <dl>
  <dt>Editor
  <dd>Aryeh Gregor &lt;ayg+spec@aryeh.name&gt;
@@ -174,8 +174,14 @@
   set.  I should be doing things like not doing anything if the selection isn't
   editable, making sure not to break out of contenteditable regions, etc.
 
-  <li>I haven't yet paid any attention to value parsing.  This is going to be
-  important to specify exactly in some cases, like with colors.
+  <li>I haven't yet paid any attention to value parsing in most cases.  This is
+  going to be important to specify exactly in some cases, like with colors.
+
+  <li>I haven't paid much attention to performance.  The algorithms here aren't
+  performance-critical in most cases, but I might have accidentally included
+  some algorithms that are too slow anyway on large pages.  Generally I haven't
+  worried about throwing nodes away and recreating them multiple times or
+  things like that, as long as it produces the correct result.
 </ul>
 
 <p class=XXX>A variety of other issues are also noted in the text, formatted
@@ -693,9 +699,11 @@
 
   <!-- If we go all the way up to the root and still don't have the desired
   value, pushing down values is pointless.  It will create extra markup for no
-  purpose. -->
+  purpose.  Except if the value is null, which basically just means "try to get
+  rid of anything affecting the current element but don't aim for any specific
+  value". -->
   <li>If the <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-parent title=concept-tree-parent>parent</a> of the last member of <var title="">ancestor list</var> is not
-  an <code class=external data-anolis-spec=domcore><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#element>Element</a></code>, abort this algorithm.
+  an <code class=external data-anolis-spec=domcore><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#element>Element</a></code>, and <var title="">new value</var> is not null, abort this algorithm.
 
   <li>While <var title="">ancestor list</var> is not empty:
 
@@ -712,7 +720,9 @@
     <li>Let <var title="">children</var> be the <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>children</a> of <var title="">current
     ancestor</var>.
 
-    <li><a href=#clear-the-value>Clear the value</a> of <var title="">current ancestor</var>.
+    <li>If the <a href=#specified-value>specified value</a> of <var title="">current ancestor</var> for
+    <var title="">command</var> is not null, <a href=#clear-the-value>clear the value</a> of
+    <var title="">current ancestor</var>.
 
     <li>For every <var title="">child</var> in <var title="">children</var>:
 
@@ -960,12 +970,14 @@
   contradicting WebKit.  This is because <span value="vertical-align:
   sub/super">, the obvious equivalent (and what WebKit uses), behaves quite
   differently: it doesn't reduce font-size, which is ugly. -->
-  <li>If <var title="">command</var> is "subscript" and <var title="">new value</var> is
-  "sub", let <var title="">new parent</var> be the result of calling <code class=external data-anolis-spec=domcore title=dom-Document-createElement><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-document-createelement>createElement("sub")</a></code> on the
+  <li>If <var title="">command</var> is "subscript" or "superscript" and <var title="">new
+  value</var> is "sub", let <var title="">new parent</var> be the result of calling
+  <code class=external data-anolis-spec=domcore title=dom-Document-createElement><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-document-createelement>createElement("sub")</a></code> on the
   <code class=external data-anolis-spec=domcore title=dom-Node-ownerDocument><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-node-ownerdocument>ownerDocument</a></code> of <var title="">node</var>.
 
-  <li>If <var title="">command</var> is "superscript" and <var title="">new value</var> is
-  "super", let <var title="">new parent</var> be the result of calling <code class=external data-anolis-spec=domcore title=dom-Document-createElement><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-document-createelement>createElement("sup")</a></code> on the
+  <li>If <var title="">command</var> is "subscript" or "superscript" and <var title="">new
+  value</var> is "super", let <var title="">new parent</var> be the result of calling
+  <code class=external data-anolis-spec=domcore title=dom-Document-createElement><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-document-createelement>createElement("sup")</a></code> on the
   <code class=external data-anolis-spec=domcore title=dom-Node-ownerDocument><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-node-ownerdocument>ownerDocument</a></code> of <var title="">node</var>.
 
   <li>If <var title="">new parent</var> is null, let <var title="">new parent</var> be the result
@@ -1455,7 +1467,7 @@
     <dl class=switch>
       <dt>1 <dd>xx-small
       <dt>2 <dd>small
-      <dt>3 <dd>normal
+      <dt>3 <dd>medium
       <dt>4 <dd>large
       <dt>5 <dd>x-large
       <dt>6 <dd>xx-large
@@ -1616,6 +1628,120 @@
 <dd><strong>Relevant CSS Property</strong>: "font-style"
 
 
+<dt><code title=""><dfn id=command-removeformat title=command-removeformat>removeFormat</dfn></code>
+<!--
+Tested in IE 9, Firefox 4.0, Chrome 12 dev, Opera 11.00.
+
+Tags stripped by everyone: b big cite code dfn em font i ins kbd samp s small
+  strike strong sub sup tt u var
+Tags left alone by everyone: br hr img
+
+Unrecognized elements: stripped by Firefox and Opera, left alone by IE and
+  Chrome.
+
+blink: stripped only by IE
+abbr: stripped only by Firefox
+a, wbr: stripped only by Opera
+
+nobr: left alone only by Firefox
+acronym, bdo, q: left alone only by Opera
+
+bdi, del, mark, span, svg: treated the same as unknown elements
+
+All elements whose default rendering is display: block are left untouched by
+all browsers (although IE seems to throw an exception on <marquee> for some
+reason).
+
+It's not clear to me why we should leave <a> alone, but everyone but Opera
+does.  In OpenOffice.org 3.2.1, doing "Default Formatting (Ctrl+M)" doesn't
+remove links.  In Microsoft Word 2007, doing "Clear Formatting" also doesn't
+remove links.  Verdict: don't remove links.  Apparently they don't logically
+qualify as "formatting".
+
+Conclusion: leave alone a, br, hr, img, wbr.  Strip everything else, including
+unrecognized elements, although of course not block elements.  Also we should
+probably treat all replaced elements the same as <img>, although I didn't test
+that (somehow I doubt it will come up much).  <video> behaves the same as
+<img>, although Firefox adds tabindex=0 (???), so I'm assuming the rest are
+similar.  Also, I'll keep all foreign elements and form elements.
+
+
+Browsers will split up all these inline elements if the selection is contained
+within them.  Opera does strip unrecognized elements with display: block if
+they're within the selection, but doesn't split them up if they contain the
+selection.
+
+Upon consideration, I've decided to go for something for now that's totally
+different from what any browser does: get rid of all elements actually
+contained in the selection (pretty much matching browsers), but for elements
+containing the selection, I'll just run all the other styling commands in a
+fashion that will reset the style in normal cases.  This avoids having to
+create a lot of new logic to decide exactly what we can split up or not, and
+should be able to correctly remove anything that can actually be created by
+these algorithms.
+
+This approach currently results in incorrect behavior in some cases for
+non-modifiable elements with default styling, like <code>.  The correct
+approach is probably to declare these elements modifiable; this would roughly
+match what browsers do.  I'm ignoring the issue for now, because such elements
+cannot actually be created by implementations of execCommand(), so they're not
+likely to be common.
+-->
+
+<dd><strong>Action</strong>:
+
+<ol>
+  <li><a href=#decompose>Decompose</a> the <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-range title=concept-range>range</a>, and let <var title="">node list</var> be the
+  result.
+
+  <li>Let <var title="">affected elements</var> be a list of all <a href=#html-element title="HTML
+  element">HTML elements</a> that are the same as or <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-descendant title=concept-tree-descendant>descendants</a> of some
+  member of <var title="">node list</var> and have non-null <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-parent title=concept-tree-parent>parents</a> and satisfy
+  (insert conditions here).
+
+  <p class=XXX>The conditions are not so simple to define, because we want to
+  include non-conforming elements, which HTML doesn't give content models.  If
+  everything had categories, we'd want something like "either it's
+  unrecognized, or it's phrasing content that's not also embedded or
+  interactive.  Except this has weird corner-cases like ins and del that are
+  sometimes phrasing and sometimes flow.
+
+  <li>For each <var title="">element</var> in <var title="">affected elements</var>:
+
+  <ol>
+    <li>While <var title="">element</var> has <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>children</a>, insert the first <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>child</a>
+    of <var title="">element</var> into the <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-parent title=concept-tree-parent>parent</a> of <var title="">element</var> immediately
+    before <var title="">element</var>, <a href=#preserving-ranges>preserving ranges</a>.
+
+    <li>Remove <var title="">element</var> from its <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-parent title=concept-tree-parent>parent</a>.
+  </ol>
+
+  <li>For each of the entries in the following table, in the given order:
+  <a href=#decompose>decompose</a> the <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-range title=concept-range>range</a> again; then <a href=#set-the-value>set the value</a>
+  of the resulting <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node title=concept-node>nodes</a>, with <var title="">command</var> and <var title="">new value</var>
+  as given.
+
+  <table>
+    <tr><th><var title="">command</var> <th><var title="">new value</var>
+    <tr><td>subscript <td>"baseline"
+    <!-- superscript not needed, subscript does the same thing.  We run this
+    first so <sub>/<sup> won't upset fontSize. -->
+    <tr><td>bold <td>"normal"
+    <tr><td>fontName <td>null
+    <tr><td>fontSize <td>null
+    <tr><td>foreColor <td>null
+    <tr><td>hiliteColor <td>null
+    <tr><td>italic <td>"normal"
+    <tr><td>strikethrough <td>null
+    <tr><td>underline <td>null
+  </table>
+</ol>
+
+<dd><strong>State</strong>:
+
+<dd><strong>Value</strong>:
+
+
 <dt><code title=""><dfn id=command-strikethrough title=command-strikethrough>strikethrough</dfn></code>
 
 <dd><strong>Action</strong>: <a href=#decompose>Decompose</a> the <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-range title=concept-range>range</a>.  If the
--- a/implementation.js	Mon Apr 11 14:48:46 2011 -0600
+++ b/implementation.js	Tue Apr 12 14:55:38 2011 -0600
@@ -1118,9 +1118,10 @@
 	}
 
 	// "If the parent of the last member of ancestor list is not an Element,
-	// abort this algorithm."
-	if (!ancestorList[ancestorList.length - 1].parentNode
-	|| ancestorList[ancestorList.length - 1].parentNode.nodeType != Node.ELEMENT_NODE) {
+	// and new value is not null, abort this algorithm."
+	if (newValue !== null
+	&& (!ancestorList[ancestorList.length - 1].parentNode
+	|| ancestorList[ancestorList.length - 1].parentNode.nodeType != Node.ELEMENT_NODE)) {
 		return;
 	}
 
@@ -1139,8 +1140,11 @@
 		// "Let children be the children of current ancestor."
 		var children = Array.prototype.slice.call(currentAncestor.childNodes);
 
-		// "Clear the value of current ancestor."
-		clearValue(currentAncestor, command);
+		// "If the specified value of current ancestor for command is not null,
+		// clear the value of current ancestor."
+		if (getSpecifiedValue(currentAncestor, command) !== null) {
+			clearValue(currentAncestor, command);
+		}
 
 		// "For every child in children:"
 		for (var i = 0; i < children.length; i++) {
@@ -1425,16 +1429,19 @@
 		}[newValue];
 	}
 
-	// "If command is "subscript" and new value is "sub", let new parent be the
-	// result of calling createElement("sub") on the ownerDocument of node."
-	if (command == "subscript" && newValue == "sub") {
+	// "If command is "subscript" or "superscript" and new value is "sub", let
+	// new parent be the result of calling createElement("sub") on the
+	// ownerDocument of node."
+	if ((command == "subscript" || command == "superscript")
+	&& newValue == "sub") {
 		newParent = node.ownerDocument.createElement("sub");
 	}
 
-	// "If command is "superscript" and new value is "super", let new parent be
-	// the result of calling createElement("sup") on the ownerDocument of
-	// node."
-	if (command == "superscript" && newValue == "super") {
+	// "If command is "subscript" or "superscript" and new value is "super",
+	// let new parent be the result of calling createElement("sup") on the
+	// ownerDocument of node."
+	if ((command == "subscript" || command == "superscript")
+	&& newValue == "super") {
 		newParent = node.ownerDocument.createElement("sup");
 	}
 
@@ -1832,6 +1839,66 @@
 		}
 		break;
 
+		case "removeformat":
+		// "Decompose the range, and let node list be the result."
+		var nodeList = decomposeRange(range);
+
+		// "Let affected elements be a list of all HTML elements that are the
+		// same as or descendants of some member of node list and have non-null
+		// parents and satisfy (insert conditions here)."
+		var affectedElements = [];
+		for (var i = 0; i < nodeList.length; i++) {
+			for (
+				var node = nodeList[i];
+				node == nodeList[i] || isDescendant(node, nodeList[i]);
+				node = nextNode(node)
+			) {
+				if (isHtmlElement(node)
+				&& node.parentNode
+				// FIXME: Extremely partial list for testing
+				&& ["A", "AUDIO", "BR", "DIV", "HR", "IMG", "P", "TD", "VIDEO", "WBR"].indexOf(node.tagName) == -1) {
+					affectedElements.push(node);
+				}
+			}
+		}
+
+		// "For each element in affected elements:"
+		for (var i = 0; i < affectedElements.length; i++) {
+			var element = affectedElements[i];
+
+			// "While element has children, insert the first child of element
+			// into the parent of element immediately before element,
+			// preserving ranges."
+			while (element.childNodes.length) {
+				movePreservingRanges(element.firstChild, element.parentNode, getNodeIndex(element));
+			}
+
+			// "Remove element from its parent."
+			element.parentNode.removeChild(element);
+		}
+
+		// "For each of the entries in the following table, in the given order:
+		// decompose the range again; then set the value of the resulting
+		// nodes, with command and new value as given."
+		var table = {
+			"subscript": "baseline",
+			"bold": "normal",
+			"fontname": null,
+			"fontsize": null,
+			"forecolor": null,
+			"hilitecolor": null,
+			"italic": "normal",
+			"strikethrough": null,
+			"underline": null,
+		};
+		for (var command in table) {
+			var nodeList = decomposeRange(range);
+			for (var i = 0; i < nodeList.length; i++) {
+				setNodeValue(nodeList[i], command, table[command]);
+			}
+		}
+		break;
+
 		case "strikethrough":
 		// "Decompose the range. If the state of the range for this command is
 		// then true, set the value of each returned node to null. Otherwise,
@@ -1956,9 +2023,9 @@
 	while (node.parentNode && node.parentNode.firstChild == node) {
 		node = node.parentNode;
 	}
-	var stop = nextNode(range.endContainer);
+	var stop = nextNodeDescendants(range.endContainer);
 
-	for (; node && node != nextNodeDescendants(range.endContainer); node = nextNode(node)) {
+	for (; node && node != stop; node = nextNode(node)) {
 		if (!isEffectivelyContained(node, range)) {
 			continue;
 		}
--- a/source.html	Mon Apr 11 14:48:46 2011 -0600
+++ b/source.html	Tue Apr 12 14:55:38 2011 -0600
@@ -162,8 +162,14 @@
   set.  I should be doing things like not doing anything if the selection isn't
   editable, making sure not to break out of contenteditable regions, etc.
 
-  <li>I haven't yet paid any attention to value parsing.  This is going to be
-  important to specify exactly in some cases, like with colors.
+  <li>I haven't yet paid any attention to value parsing in most cases.  This is
+  going to be important to specify exactly in some cases, like with colors.
+
+  <li>I haven't paid much attention to performance.  The algorithms here aren't
+  performance-critical in most cases, but I might have accidentally included
+  some algorithms that are too slow anyway on large pages.  Generally I haven't
+  worried about throwing nodes away and recreating them multiple times or
+  things like that, as long as it produces the correct result.
 </ul>
 
 <p class=XXX>A variety of other issues are also noted in the text, formatted
@@ -686,9 +692,11 @@
 
   <!-- If we go all the way up to the root and still don't have the desired
   value, pushing down values is pointless.  It will create extra markup for no
-  purpose. -->
+  purpose.  Except if the value is null, which basically just means "try to get
+  rid of anything affecting the current element but don't aim for any specific
+  value". -->
   <li>If the [[parent]] of the last member of <var>ancestor list</var> is not
-  an [[element]], abort this algorithm.
+  an [[element]], and <var>new value</var> is not null, abort this algorithm.
 
   <li>While <var>ancestor list</var> is not empty:
 
@@ -705,7 +713,9 @@
     <li>Let <var>children</var> be the [[children]] of <var>current
     ancestor</var>.
 
-    <li><span>Clear the value</span> of <var>current ancestor</var>.
+    <li>If the <span>specified value</span> of <var>current ancestor</var> for
+    <var>command</var> is not null, <span>clear the value</span> of
+    <var>current ancestor</var>.
 
     <li>For every <var>child</var> in <var>children</var>:
 
@@ -966,15 +976,15 @@
   contradicting WebKit.  This is because <span value="vertical-align:
   sub/super">, the obvious equivalent (and what WebKit uses), behaves quite
   differently: it doesn't reduce font-size, which is ugly. -->
-  <li>If <var>command</var> is "subscript" and <var>new value</var> is
-  "sub", let <var>new parent</var> be the result of calling <code
-  data-anolis-spec=domcore
+  <li>If <var>command</var> is "subscript" or "superscript" and <var>new
+  value</var> is "sub", let <var>new parent</var> be the result of calling
+  <code data-anolis-spec=domcore
   title=dom-Document-createElement>createElement("sub")</code> on the
   [[ownerdocument]] of <var>node</var>.
 
-  <li>If <var>command</var> is "superscript" and <var>new value</var> is
-  "super", let <var>new parent</var> be the result of calling <code
-  data-anolis-spec=domcore
+  <li>If <var>command</var> is "subscript" or "superscript" and <var>new
+  value</var> is "super", let <var>new parent</var> be the result of calling
+  <code data-anolis-spec=domcore
   title=dom-Document-createElement>createElement("sup")</code> on the
   [[ownerdocument]] of <var>node</var>.
 
@@ -1470,7 +1480,7 @@
     <dl class=switch>
       <dt>1 <dd>xx-small
       <dt>2 <dd>small
-      <dt>3 <dd>normal
+      <dt>3 <dd>medium
       <dt>4 <dd>large
       <dt>5 <dd>x-large
       <dt>6 <dd>xx-large
@@ -1637,6 +1647,120 @@
 <dd><strong>Relevant CSS Property</strong>: "font-style"
 
 
+<dt><code title><dfn title=command-removeformat>removeFormat</dfn></code>
+<!--
+Tested in IE 9, Firefox 4.0, Chrome 12 dev, Opera 11.00.
+
+Tags stripped by everyone: b big cite code dfn em font i ins kbd samp s small
+  strike strong sub sup tt u var
+Tags left alone by everyone: br hr img
+
+Unrecognized elements: stripped by Firefox and Opera, left alone by IE and
+  Chrome.
+
+blink: stripped only by IE
+abbr: stripped only by Firefox
+a, wbr: stripped only by Opera
+
+nobr: left alone only by Firefox
+acronym, bdo, q: left alone only by Opera
+
+bdi, del, mark, span, svg: treated the same as unknown elements
+
+All elements whose default rendering is display: block are left untouched by
+all browsers (although IE seems to throw an exception on <marquee> for some
+reason).
+
+It's not clear to me why we should leave <a> alone, but everyone but Opera
+does.  In OpenOffice.org 3.2.1, doing "Default Formatting (Ctrl+M)" doesn't
+remove links.  In Microsoft Word 2007, doing "Clear Formatting" also doesn't
+remove links.  Verdict: don't remove links.  Apparently they don't logically
+qualify as "formatting".
+
+Conclusion: leave alone a, br, hr, img, wbr.  Strip everything else, including
+unrecognized elements, although of course not block elements.  Also we should
+probably treat all replaced elements the same as <img>, although I didn't test
+that (somehow I doubt it will come up much).  <video> behaves the same as
+<img>, although Firefox adds tabindex=0 (???), so I'm assuming the rest are
+similar.  Also, I'll keep all foreign elements and form elements.
+
+
+Browsers will split up all these inline elements if the selection is contained
+within them.  Opera does strip unrecognized elements with display: block if
+they're within the selection, but doesn't split them up if they contain the
+selection.
+
+Upon consideration, I've decided to go for something for now that's totally
+different from what any browser does: get rid of all elements actually
+contained in the selection (pretty much matching browsers), but for elements
+containing the selection, I'll just run all the other styling commands in a
+fashion that will reset the style in normal cases.  This avoids having to
+create a lot of new logic to decide exactly what we can split up or not, and
+should be able to correctly remove anything that can actually be created by
+these algorithms.
+
+This approach currently results in incorrect behavior in some cases for
+non-modifiable elements with default styling, like <code>.  The correct
+approach is probably to declare these elements modifiable; this would roughly
+match what browsers do.  I'm ignoring the issue for now, because such elements
+cannot actually be created by implementations of execCommand(), so they're not
+likely to be common.
+-->
+
+<dd><strong>Action</strong>:
+
+<ol>
+  <li><span>Decompose</span> the [[range]], and let <var>node list</var> be the
+  result.
+
+  <li>Let <var>affected elements</var> be a list of all <span title="HTML
+  element">HTML elements</span> that are the same as or [[descendants]] of some
+  member of <var>node list</var> and have non-null [[parents]] and satisfy
+  (insert conditions here).
+
+  <p class=XXX>The conditions are not so simple to define, because we want to
+  include non-conforming elements, which HTML doesn't give content models.  If
+  everything had categories, we'd want something like "either it's
+  unrecognized, or it's phrasing content that's not also embedded or
+  interactive.  Except this has weird corner-cases like ins and del that are
+  sometimes phrasing and sometimes flow.
+
+  <li>For each <var>element</var> in <var>affected elements</var>:
+
+  <ol>
+    <li>While <var>element</var> has [[children]], insert the first [[child]]
+    of <var>element</var> into the [[parent]] of <var>element</var> immediately
+    before <var>element</var>, <span>preserving ranges</span>.
+
+    <li>Remove <var>element</var> from its [[parent]].
+  </ol>
+
+  <li>For each of the entries in the following table, in the given order:
+  <span>decompose</span> the [[range]] again; then <span>set the value</span>
+  of the resulting [[nodes]], with <var>command</var> and <var>new value</var>
+  as given.
+
+  <table>
+    <tr><th><var>command</var> <th><var>new value</var>
+    <tr><td>subscript <td>"baseline"
+    <!-- superscript not needed, subscript does the same thing.  We run this
+    first so <sub>/<sup> won't upset fontSize. -->
+    <tr><td>bold <td>"normal"
+    <tr><td>fontName <td>null
+    <tr><td>fontSize <td>null
+    <tr><td>foreColor <td>null
+    <tr><td>hiliteColor <td>null
+    <tr><td>italic <td>"normal"
+    <tr><td>strikethrough <td>null
+    <tr><td>underline <td>null
+  </table>
+</ol>
+
+<dd><strong>State</strong>:
+
+<dd><strong>Value</strong>:
+
+
 <dt><code title><dfn title=command-strikethrough>strikethrough</dfn></code>
 
 <dd><strong>Action</strong>: <span>Decompose</span> the [[range]].  If the