Initial formatBlock support
authorAryeh Gregor <AryehGregor+gitcommit@gmail.com>
Mon, 23 May 2011 15:18:11 -0600
changeset 164 a841873c905a
parent 163 b8ce2db14e48
child 165 1fc969f30231
Initial formatBlock support
autoimplementation.html
editcommands.html
implementation.js
preprocess
source.html
--- a/autoimplementation.html	Mon May 23 14:50:26 2011 -0600
+++ b/autoimplementation.html	Mon May 23 15:18:11 2011 -0600
@@ -466,6 +466,181 @@
 		'foo<span id=purple>ba[r</span>ba]z',
 		'<span style="color: rgb(255, 0, 0)">foo<span id=purple>b[a]r</span>baz</span>',
 	],
+	formatblock: [
+		'foo[]bar<p>extra',
+		'<span>foo</span>{}<span>bar</span><p>extra',
+		'<span>foo[</span><span>]bar</span><p>extra',
+		'foo[bar]baz<p>extra',
+		'foo]bar[baz<p>extra',
+		'{<p><p> <p>foo</p>}',
+		'foo[bar<i>baz]qoz</i>quz<p>extra',
+
+		'<table><tbody><tr><td>foo<td>b[a]r<td>baz</table>',
+		'<table><tbody><tr data-start=1 data-end=2><td>foo<td>bar<td>baz</table>',
+		'<table><tbody><tr data-start=0 data-end=2><td>foo<td>bar<td>baz</table>',
+		'<table><tbody data-start=0 data-end=1><tr><td>foo<td>bar<td>baz</table>',
+		'<table data-start=0 data-end=1><tbody><tr><td>foo<td>bar<td>baz</table>',
+		'{<table><tr><td>foo<td>bar<td>baz</table>}',
+
+		'<div>[foobar]</div>',
+		'<p>[foobar]</p>',
+		'<blockquote>[foobar]</blockquote>',
+		'<h1>[foobar]</h1>',
+		'<h2>[foobar]</h2>',
+		'<h3>[foobar]</h3>',
+		'<h4>[foobar]</h4>',
+		'<h5>[foobar]</h5>',
+		'<h6>[foobar]</h6>',
+		'<dl><dt>[foo]<dd>bar</dl>',
+		'<dl><dt>foo<dd>[bar]</dl>',
+		'<dl><dt>[foo<dd>bar]</dl>',
+		'<ol><li>[foobar]</ol>',
+		'<ul><li>[foobar]</ul>',
+		'<address>[foobar]</address>',
+		'<pre>[foobar]</pre>',
+		'<article>[foobar]</article>',
+		'<ins>[foobar]</ins>',
+		'<del>[foobar]</del>',
+		'<quasit>[foobar]</quasit>',
+		'<quasit style="display: block">[foobar]</quasit>',
+
+		['<p>', 'foo[]bar<p>extra'],
+		['<p>', '<span>foo</span>{}<span>bar</span><p>extra'],
+		['<p>', '<span>foo[</span><span>]bar</span><p>extra'],
+		['<p>', 'foo[bar]baz<p>extra'],
+		['<p>', 'foo]bar[baz<p>extra'],
+		['<p>', '{<p><p> <p>foo</p>}'],
+		['<p>', 'foo[bar<i>baz]qoz</i>quz<p>extra'],
+
+		['<p>', '<table><tbody><tr><td>foo<td>b[a]r<td>baz</table>'],
+		['<p>', '<table><tbody><tr data-start=1 data-end=2><td>foo<td>bar<td>baz</table>'],
+		['<p>', '<table><tbody><tr data-start=0 data-end=2><td>foo<td>bar<td>baz</table>'],
+		['<p>', '<table><tbody data-start=0 data-end=1><tr><td>foo<td>bar<td>baz</table>'],
+		['<p>', '<table data-start=0 data-end=1><tbody><tr><td>foo<td>bar<td>baz</table>'],
+		['<p>', '{<table><tr><td>foo<td>bar<td>baz</table>}'],
+
+		['<p>', '<div>[foobar]</div>'],
+		['<p>', '<p>[foobar]</p>'],
+		['<p>', '<blockquote>[foobar]</blockquote>'],
+		['<p>', '<h1>[foobar]</h1>'],
+		['<p>', '<h2>[foobar]</h2>'],
+		['<p>', '<h3>[foobar]</h3>'],
+		['<p>', '<h4>[foobar]</h4>'],
+		['<p>', '<h5>[foobar]</h5>'],
+		['<p>', '<h6>[foobar]</h6>'],
+		['<p>', '<dl><dt>[foo]<dd>bar</dl>'],
+		['<p>', '<dl><dt>foo<dd>[bar]</dl>'],
+		['<p>', '<dl><dt>[foo<dd>bar]</dl>'],
+		['<p>', '<ol><li>[foobar]</ol>'],
+		['<p>', '<ul><li>[foobar]</ul>'],
+		['<p>', '<address>[foobar]</address>'],
+		['<p>', '<pre>[foobar]</pre>'],
+		['<p>', '<article>[foobar]</article>'],
+		['<p>', '<ins>[foobar]</ins>'],
+		['<p>', '<del>[foobar]</del>'],
+		['<p>', '<quasit>[foobar]</quasit>'],
+		['<p>', '<quasit style="display: block">[foobar]</quasit>'],
+
+		['<blockquote>', '<blockquote>[foo]</blockquote><p>extra'],
+		['<blockquote>', '<blockquote><p>[foo]<p>bar</blockquote><p>extra'],
+		['<blockquote>', '[foo]<blockquote>bar</blockquote><p>extra'],
+		['<blockquote>', '<p>[foo<p>bar]<p>baz'],
+		['<blockquote>', '<section>[foo]</section>'],
+		['<blockquote>', '<section><p>[foo]</section>'],
+		['<blockquote>', '<section><hgroup><h1>[foo]</h1><h2>bar</h2></hgroup><p>baz</section>'],
+		['<article>', '<section>[foo]</section>'],
+
+		['<p>', '<div>[foobar]</div>'],
+		['<blockquote>', '<div>[foobar]</div>'],
+		['<h1>', '<div>[foobar]</div>'],
+		['<h2>', '<div>[foobar]</div>'],
+		['<h3>', '<div>[foobar]</div>'],
+		['<h4>', '<div>[foobar]</div>'],
+		['<h5>', '<div>[foobar]</div>'],
+		['<h6>', '<div>[foobar]</div>'],
+		['<dl>', '<div>[foobar]</div>'],
+		['<dt>', '<div>[foobar]</div>'],
+		['<dd>', '<div>[foobar]</div>'],
+		['<ol>', '<div>[foobar]</div>'],
+		['<ul>', '<div>[foobar]</div>'],
+		['<li>', '<div>[foobar]</div>'],
+		['<address>', '<div>[foobar]</div>'],
+		['<pre>', '<div>[foobar]</div>'],
+		['<article>', '<div>[foobar]</div>'],
+		['<ins>', '<div>[foobar]</div>'],
+		['<del>', '<div>[foobar]</div>'],
+		['<quasit>', '<div>[foobar]</div>'],
+
+		['<div>', '<p>[foobar]</p>'],
+		['<p>', '<p>[foobar]</p>'],
+		['<blockquote>', '<p>[foobar]</p>'],
+		['<h1>', '<p>[foobar]</p>'],
+		['<h2>', '<p>[foobar]</p>'],
+		['<h3>', '<p>[foobar]</p>'],
+		['<h4>', '<p>[foobar]</p>'],
+		['<h5>', '<p>[foobar]</p>'],
+		['<h6>', '<p>[foobar]</p>'],
+		['<dl>', '<p>[foobar]</p>'],
+		['<dt>', '<p>[foobar]</p>'],
+		['<dd>', '<p>[foobar]</p>'],
+		['<ol>', '<p>[foobar]</p>'],
+		['<ul>', '<p>[foobar]</p>'],
+		['<li>', '<p>[foobar]</p>'],
+		['<address>', '<p>[foobar]</p>'],
+		['<pre>', '<p>[foobar]</p>'],
+		['<ins>', '<p>[foobar]</p>'],
+		['<del>', '<p>[foobar]</p>'],
+		['<quasit>', '<p>[foobar]</p>'],
+		['<article>', '<p>[foobar]</p>'],
+		['<aside>', '<p>[foobar]</p>'],
+		['<body>', '<p>[foobar]</p>'],
+		['<figcaption>', '<p>[foobar]</p>'],
+		['<figure>', '<p>[foobar]</p>'],
+		['<footer>', '<p>[foobar]</p>'],
+		['<header>', '<p>[foobar]</p>'],
+		['<head>', '<p>[foobar]</p>'],
+		['<hgroup>', '<p>[foobar]</p>'],
+		['<html>', '<p>[foobar]</p>'],
+		['<nav>', '<p>[foobar]</p>'],
+		['<section>', '<p>[foobar]</p>'],
+
+		['<div>', '<p>[foo<p>bar]'],
+		['<p>', '<p>[foo<p>bar]'],
+		['<blockquote>', '<p>[foo<p>bar]'],
+		['<h1>', '<p>[foo<p>bar]'],
+		['<h2>', '<p>[foo<p>bar]'],
+		['<h3>', '<p>[foo<p>bar]'],
+		['<h4>', '<p>[foo<p>bar]'],
+		['<h5>', '<p>[foo<p>bar]'],
+		['<h6>', '<p>[foo<p>bar]'],
+		['<dl>', '<p>[foo<p>bar]'],
+		['<dt>', '<p>[foo<p>bar]'],
+		['<dd>', '<p>[foo<p>bar]'],
+		['<ol>', '<p>[foo<p>bar]'],
+		['<ul>', '<p>[foo<p>bar]'],
+		['<li>', '<p>[foo<p>bar]'],
+		['<address>', '<p>[foo<p>bar]'],
+		['<pre>', '<p>[foo<p>bar]'],
+		['<ins>', '<p>[foo<p>bar]'],
+		['<del>', '<p>[foo<p>bar]'],
+		['<quasit>', '<p>[foo<p>bar]'],
+		['<article>', '<p>[foo<p>bar]'],
+		['<aside>', '<p>[foo<p>bar]'],
+		['<body>', '<p>[foo<p>bar]'],
+		['<figcaption>', '<p>[foo<p>bar]'],
+		['<figure>', '<p>[foo<p>bar]'],
+		['<footer>', '<p>[foo<p>bar]'],
+		['<header>', '<p>[foo<p>bar]'],
+		['<head>', '<p>[foo<p>bar]'],
+		['<hgroup>', '<p>[foo<p>bar]'],
+		['<html>', '<p>[foo<p>bar]'],
+		['<nav>', '<p>[foo<p>bar]'],
+		['<section>', '<p>[foo<p>bar]'],
+
+		['p', '<div>[foobar]</div>'],
+
+		'<ol><li>[foo]<li>bar</ol>',
+	],
 	hilitecolor: [
 		'foo[]bar',
 		'<span>foo</span>{}<span>bar</span>',
@@ -1572,6 +1747,7 @@
 	fontname: "sans-serif",
 	fontsize: "4",
 	forecolor: "#FF0000",
+	formatblock: "<div>",
 	hilitecolor: "#FF8888",
 	inserthorizontalrule: "",
 	insertimage: "/img/lion.svg",
--- a/editcommands.html	Mon May 23 14:50:26 2011 -0600
+++ b/editcommands.html	Mon May 23 15:18:11 2011 -0600
@@ -40,7 +40,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-22-may-2011>Work in Progress &mdash; Last Update 22 May 2011</h2>
+<h2 class="no-num no-toc" id=work-in-progress-&mdash;-last-update-23-may-2011>Work in Progress &mdash; Last Update 23 May 2011</h2>
 <dl>
  <dt>Editor
  <dd>Aryeh Gregor &lt;[email protected]&gt;
@@ -105,10 +105,11 @@
    <li><a href=#outdenting-a-node><span class=secno>7.3 </span>Outdenting a node</a></li>
    <li><a href=#block-extending-a-range><span class=secno>7.4 </span>Block-extending a range</a></li>
    <li><a href=#toggling-lists><span class=secno>7.5 </span>Toggling lists</a></li>
-   <li><a href=#the-indent-command><span class=secno>7.6 </span>The <code title="">indent</code> command</a></li>
-   <li><a href=#the-insertorderedlist-command><span class=secno>7.7 </span>The <code title="">insertOrderedList</code> command</a></li>
-   <li><a href=#the-insertunorderedlist-command><span class=secno>7.8 </span>The <code title="">insertUnorderedList</code> command</a></li>
-   <li><a href=#the-outdent-command><span class=secno>7.9 </span>The <code title="">outdent</code> command</a></ol></li>
+   <li><a href=#the-formatblock-command><span class=secno>7.6 </span>The <code title="">formatBlock</code> command</a></li>
+   <li><a href=#the-indent-command><span class=secno>7.7 </span>The <code title="">indent</code> command</a></li>
+   <li><a href=#the-insertorderedlist-command><span class=secno>7.8 </span>The <code title="">insertOrderedList</code> command</a></li>
+   <li><a href=#the-insertunorderedlist-command><span class=secno>7.9 </span>The <code title="">insertUnorderedList</code> command</a></li>
+   <li><a href=#the-outdent-command><span class=secno>7.10 </span>The <code title="">outdent</code> command</a></ol></li>
  <li><a href=#miscellaneous-commands><span class=secno>8 </span>Miscellaneous commands</a>
   <ol>
    <li><a href=#the-stylewithcss-command><span class=secno>8.1 </span>The <code title="">styleWithCSS</code> command</a></li>
@@ -659,9 +660,9 @@
 * copy, cut, paste: Needs attention to security.
 * delete, redo, undo: Needs review of the Google work on this; will probably be
   quite complicated.
-* formatBlock, insertParagraph, justifyCenter, justifyFull, justifyLeft,
-  justifyRight: These look important and should be similar to indent, so
-  they're next on my list.
+* insertParagraph, justifyCenter, justifyFull, justifyLeft, justifyRight: These
+  look important and should be similar to work I've already done, so they're
+  next on my list.
 * insertHTML: Not supported by IE, but important.  I've seen frameworks that
   work around its absence in IE by using insertImage and then search and
   replace.  Will probably need to be defined in terms of insertAdjacentHTML or
@@ -2453,6 +2454,10 @@
   attribute.
 </ul>
 
+<p>A <dfn id=single-line-container>single-line container</dfn> is an <a href=#html-element>HTML element</a> with
+<a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-element-local-name title=concept-element-local-name>local name</a> "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "p", or
+"pre".
+
 
 <h3 id=assorted-block-formatting-command-algorithms><span class=secno>7.2 </span>Assorted block formatting command algorithms</h3>
 
@@ -3435,7 +3440,134 @@
 </ol>
 
 
-<h3 id=the-indent-command><span class=secno>7.6 </span><dfn>The <code title="">indent</code> command</dfn></h3>
+<h3 id=the-formatblock-command><span class=secno>7.6 </span><dfn>The <code title="">formatBlock</code> command</dfn></h3>
+<!--
+Tested browser versions: IE9, Firefox 4.0, Chrome 13 dev, Opera 11.10.
+
+Firefox and Chrome will replace a <blockquote> by a <p> or other given tag.  IE
+and Opera will nest the <p> inside instead.  The latter makes more sense, given
+that a) we don't support formatBlock with <blockquote> and b) <blockquote>s are
+logically different, since they can contain many lines.
+
+Firefox will not convert other tags like <p> to <div>, it will only wrap
+unwrapped lines in a <div>.  Firefox also won't replace <div> by things like
+<p>, it will nest the <p> inside.  The spec follows other browsers.
+
+If you try to convert a <dt> to a <div> or <p> or such, Firefox breaks out of
+the <dl> entirely, leaving ...<dt><br></dt></dl>.  Chrome will convert a <dt>
+or <dd> to the given element, leaving a <div> or <p> or such as the child of a
+<dl>.  I follow IE/Opera, which only affect the contents of <dt>/<dd> (Firefox
+behaves this way for <dd> as well, just not <dt>).  This means you can get
+invalid DOMs like <dt><p>foo<p></dt>, but they can be serialized as text/html,
+so I'm not too fussy.
+
+When it comes to <li>, IE/Opera behave like with <dt>/<dd>, which is how I
+behave too.  Firefox apparently refuses to do anything.  Chrome tries to wrap
+the parent list element, breaking it up if only some of the children are
+selected; this produces unserializable DOMs if you're wrapping with <p>.
+
+When you're converting multiple blocks at once, Chrome replaces them all by one
+block with <br> stuck in, like <p>foo</p><p>bar</p> -> <div>foo<br>bar</div>.
+It wipes out intervening block containers too in some cases.  This might make
+sense for <address>/<h*>/<pre>, but other browsers don't do it.
+-->
+
+<p><a href=#action>Action</a>:
+
+<ol>
+  <li>If <var title="">value</var> begins with a "&lt;" character and ends with a "&gt;"
+  character, remove the first and last characters from it.
+  <!-- IE9 requires the brackets.  If they're not provided, it does nothing.
+  -->
+
+  <li>Let <var title="">value</var> be <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#converted-to-lowercase>converted to
+  lowercase</a>.
+
+  <li>If <var title="">value</var> is not "address", "div", "h1", "h2", "h3", "h4",
+  "h5", "h6", "p", or "pre", then do nothing and abort these steps.
+
+  <p class=XXX>Non-IE browsers support a lot more elements, but I have my
+  doubts about how much sense it makes to support things where you'd expect to
+  nest them.  Will send mail to the list.
+  <!--
+  Opera 11.10 throws NOT_SUPPORTED_ERR for bad elements, all other tested
+  browsers ignore the input.  Testing in IE9, Firefox 4.0, Chrome 13 dev, and
+  Opera 11.10, supported elements seem to be:
+
+  Everyone: address, div, h*, p, pre
+  Everyone but IE: blockquote
+  Everyone but Opera: dd, dt
+  IE only: ol, ul
+  Firefox and Chrome only: dl
+  Chrome only: article, aside, footer, header, hgroup, nav, section
+
+  HTML5 as of May 2011 supports: address, article, aside, blockquote, div,
+  footer, h*, header, hgroup, nav, p, pre, section, which exactly matches
+  Chrome except minus dd/dt/dl.
+  -->
+
+  <li><a href=#block-extend>Block-extend</a> the <a href=#active-range>active range</a>, and let <var title="">new
+  range</var> be the result.
+
+  <li>Let <var title="">node list</var> be an empty list of <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>.
+
+  <li>For each <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node title=concept-node>node</a> <var title="">node</var> <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#contained>contained</a> in <var title="">new range</var>,
+  append <var title="">node</var> to <var title="">node list</var> if it is
+  <a href=#editable>editable</a>, the last member of <var title="">node list</var> (if any) is
+  not an <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-ancestor title=concept-tree-ancestor>ancestor</a> of <var title="">node</var>, and <var title="">node</var> is not an
+  <a href=#html-element>HTML element</a> that has <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-element-local-name title=concept-element-local-name>local name</a> "article", "aside",
+  "blockquote", "caption", "col", "colgroup", "dd", "dl", "dt", "footer",
+  "header", "hgroup", "li", "nav", "ol", "section", "table", "tbody", "td",
+  "th", "thead", "tr", or "ul".
+
+  <li>While <var title="">node list</var> is not empty:
+
+  <ol>
+    <li>If the first member of <var title="">node list</var> is a <a href=#single-line-container>single-line
+    container</a>, <a href=#set-the-tag-name>set the tag name</a> of the first member of
+    <var title="">node list</var> to <var title="">value</var>, then remove the first member from
+    <var title="">node list</var> and continue this loop from the beginning.
+
+    <div class=XXX>
+    <p>This implies that
+
+    </p><xmp><p>foo</p><p>bar</p></xmp>
+
+    <p>becomes (for instance)
+
+    </p><xmp><h1>foo</h1><h1>bar</h1></xmp>
+
+    <p>even though
+
+    </p><xmp><h1>foo<br>bar</h1></xmp>
+
+    <p>would probably make more sense.  WebKit has the latter behavior in all
+    cases.  Perhaps we should adopt WebKit's behavior for things where it
+    rarely makes sense to have two in a row, while keeping the current behavior
+    for divs and paragraphs?
+    </div>
+
+    <li>Let <var title="">sublist</var> be an empty list of <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>.
+
+    <li>Remove the first member of <var title="">node list</var> and append it to
+    <var title="">sublist</var>.
+
+    <li>While <var title="">node list</var> is not empty, and the first member of
+    <var title="">node list</var> is the <code class=external data-anolis-spec=domcore title=dom-Node-nextSibling><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-node-nextsibling>nextSibling</a></code> of the last member of
+    <var title="">sublist</var>, and the first member of <var title="">node list</var> is not a
+    <a href=#single-line-container>single-line container</a>, and the last member of
+    <var title="">sublist</var> is not a <code class=external data-anolis-spec=html title="the br element"><a href=http://www.whatwg.org/html/#the-br-element>br</a></code>, remove the first member of <var title="">node
+    list</var> and append it to <var title="">sublist</var>.
+
+    <li><a href=#wrap>Wrap</a> <var title="">sublist</var>, with <a href=#sibling-criteria>sibling
+    criteria</a> matching nothing and <a href=#new-parent-instructions>new parent instructions</a>
+    returning the result of running <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(<var title="">value</var>)</a></code> on
+    the <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#context-object>context object</a>.
+  </ol>
+</ol>
+
+
+<h3 id=the-indent-command><span class=secno>7.7 </span><dfn>The <code title="">indent</code> command</dfn></h3>
 <!--
 IE9: Outputs <blockquote style="margin-right: 0px" dir="ltr">, or when
   surrounding RTL blocks, <blockquote style="margin-left: 0px" dir="rtl">.  The
@@ -3532,19 +3664,19 @@
 </ol>
 
 
-<h3 id=the-insertorderedlist-command><span class=secno>7.7 </span><dfn>The <code title="">insertOrderedList</code> command</dfn></h3>
+<h3 id=the-insertorderedlist-command><span class=secno>7.8 </span><dfn>The <code title="">insertOrderedList</code> command</dfn></h3>
 
 <p><a href=#action>Action</a>: <a href=#toggle-lists>Toggle lists</a> with <var title="">tag name</var>
 "ol".
 
 
-<h3 id=the-insertunorderedlist-command><span class=secno>7.8 </span><dfn>The <code title="">insertUnorderedList</code> command</dfn></h3>
+<h3 id=the-insertunorderedlist-command><span class=secno>7.9 </span><dfn>The <code title="">insertUnorderedList</code> command</dfn></h3>
 
 <p><a href=#action>Action</a>: <a href=#toggle-lists>Toggle lists</a> with <var title="">tag name</var>
 "ul".
 
 
-<h3 id=the-outdent-command><span class=secno>7.9 </span><dfn>The <code title="">outdent</code> command</dfn></h3>
+<h3 id=the-outdent-command><span class=secno>7.10 </span><dfn>The <code title="">outdent</code> command</dfn></h3>
 
 <p><a href=#action>Action</a>:
 
--- a/implementation.js	Mon May 23 14:50:26 2011 -0600
+++ b/implementation.js	Mon May 23 15:18:11 2011 -0600
@@ -301,6 +301,48 @@
 		&& pos2 == "before";
 }
 
+/**
+ * Return all editable nodes contained in range that the provided function
+ * returns true for, omitting any with an ancestor already being returned.
+ */
+function collectContainedNodes(range, condition) {
+	var node = range.startContainer;
+	if (node.hasChildNodes()
+	&& range.startOffset < node.childNodes.length) {
+		// A child is contained
+		node = node.childNodes[range.startOffset];
+	} else if (range.startOffset == getNodeLength(node)) {
+		// No descendant can be contained
+		node = nextNodeDescendants(node);
+	} else {
+		// No children; this node at least can't be contained
+		node = nextNode(node);
+	}
+
+	var stop = range.endContainer;
+	if (stop.hasChildNodes()
+	&& range.endOffset < node.childNodes.length) {
+		// The node after the last contained node is a child
+		stop = stop.childNodes[range.endOffset];
+	} else {
+		// This node and/or some of its children might be contained
+		stop = nextNodeDescendants(node);
+	}
+
+	var nodeList = [];
+	while (isBefore(node, stop)) {
+		if (isContained(node, range)
+		&& isEditable(node)
+		&& condition(node)) {
+			nodeList.push(node);
+			node = nextNodeDescendants(node);
+			continue;
+		}
+		node = nextNode(node);
+	}
+	return nodeList;
+}
+
 
 function parseSimpleColor(color) {
 	// This is stupid, but otherwise my automated tests will have places where
@@ -2384,6 +2426,7 @@
 		break;
 
 		case "forecolor":
+		// See later for formatblock
 		case "hilitecolor":
 		// "If value is not a valid CSS color, prepend "#" to it."
 		//
@@ -2411,6 +2454,84 @@
 		}
 		break;
 
+		case "formatblock":
+		var singleLineContainerNames = ["ADDRESS", "DIV", "H1", "H2", "H3",
+			"H4", "H5", "H6", "P", "PRE"];
+
+		// "If value begins with a "<" character and ends with a ">" character,
+		// remove the first and last characters from it."
+		if (/^<.*>$/.test(value)) {
+			value = value.slice(1, -1);
+		}
+
+		// "Let value be converted to lowercase."
+		value = value.toLowerCase();
+
+		// "If value is not "address", "div", "h1", "h2", "h3", "h4", "h5",
+		// "h6", "p", or "pre", then do nothing and abort these steps."
+		if (singleLineContainerNames.indexOf(value.toUpperCase()) == -1) {
+			return;
+		}
+
+		// "Block-extend the active range, and let new range be the result."
+		var newRange = blockExtendRange(range);
+
+		// "Let node list be an empty list of nodes."
+		var nodeList = [];
+
+		// "For each node node contained in new range, append node to node list
+		// if it is editable, the last member of node list (if any) is not an
+		// ancestor of node, and node is not an HTML element that has local
+		// name "article", "aside", "blockquote", "caption", "col", "colgroup",
+		// "dd", "dl", "dt", "footer", "header", "hgroup", "li", "nav", "ol",
+		// "section", "table", "tbody", "td", "th", "thead", "tr", or "ul"."
+		nodeList = collectContainedNodes(newRange, function(node) {
+			return !isHtmlElement(node, ["ARTICLE", "ASIDE", "BLOCKQUOTE",
+				"CAPTION", "COL", "COLGROUP", "DD", "DL", "DT", "FOOTER",
+				"HEADER", "HGROUP", "LI", "NAV", "OL", "SECTION", "TABLE",
+				"TBODY", "TD", "TH", "THEAD", "TR", "UL"]);
+		});
+
+		// "While node list is not empty:"
+		while (nodeList.length) {
+			// "If the first member of node list is a single-line container,
+			// set the tag name of the first member of node list to value, then
+			// remove the first member from node list and continue this loop
+			// from the beginning."
+			if (isHtmlElement(nodeList[0], singleLineContainerNames)) {
+				setTagName(nodeList[0], value);
+				nodeList.shift();
+				continue;
+			}
+
+			// "Let sublist be an empty list of nodes."
+			var sublist = [];
+
+			// "Remove the first member of node list and append it to
+			// sublist."
+			sublist.push(nodeList.shift());
+
+			// "While node list is not empty, and the first member of node list
+			// is the nextSibling of the last member of sublist, and the first
+			// member of node list is not a single-line container, and the last
+			// member of sublist is not a br, remove the first member of node
+			// list and append it to sublist."
+			while (nodeList.length
+			&& nodeList[0] == sublist[sublist.length - 1].nextSibling
+			&& !isHtmlElement(nodeList[0], singleLineContainerNames)
+			&& !isHtmlElement(sublist[sublist.length - 1], "BR")) {
+				sublist.push(nodeList.shift());
+			}
+
+			// "Wrap sublist, with sibling criteria matching nothing and
+			// new parent instructions returning the result of running
+			// createElement(value) on the context object."
+			wrap(sublist,
+				function() { return false },
+				function() { return document.createElement(value) });
+		}
+		break;
+
 		case "indent":
 		// "Let items be a list of all lis that are ancestor containers of the
 		// range's start and/or end node."
--- a/preprocess	Mon May 23 14:50:26 2011 -0600
+++ b/preprocess	Mon May 23 15:18:11 2011 -0600
@@ -28,6 +28,7 @@
     'descendant': '<span data-anolis-spec=domcore title=concept-tree-descendant>descendant</span>',
     'directionality': '<span data-anolis-spec=html title="the directionality">directionality</span>',
     'div': '<code data-anolis-spec=html title="the div element">div</code>',
+    'dl': '<code data-anolis-spec=html title="the dl element">dl</code>',
     'document': '<code data-anolis-spec=domcore>Document</code>',
     'documentfragment': '<code data-anolis-spec=domcore>DocumentFragment</code>',
     'element': '<code data-anolis-spec=domcore>Element</code>',
--- a/source.html	Mon May 23 14:50:26 2011 -0600
+++ b/source.html	Mon May 23 15:18:11 2011 -0600
@@ -624,9 +624,9 @@
 * copy, cut, paste: Needs attention to security.
 * delete, redo, undo: Needs review of the Google work on this; will probably be
   quite complicated.
-* formatBlock, insertParagraph, justifyCenter, justifyFull, justifyLeft,
-  justifyRight: These look important and should be similar to indent, so
-  they're next on my list.
+* insertParagraph, justifyCenter, justifyFull, justifyLeft, justifyRight: These
+  look important and should be similar to work I've already done, so they're
+  next on my list.
 * insertHTML: Not supported by IE, but important.  I've seen frameworks that
   work around its absence in IE by using insertImage and then search and
   replace.  Will probably need to be defined in terms of insertAdjacentHTML or
@@ -2457,6 +2457,10 @@
   attribute.
 </ul>
 
+<p>A <dfn>single-line container</dfn> is an <span>HTML element</span> with
+[[localname]] "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "p", or
+"pre".
+
 
 <h3>Assorted block formatting command algorithms</h3>
 
@@ -3462,6 +3466,134 @@
 </ol>
 
 
+<h3><dfn>The <code title>formatBlock</code> command</dfn></h3>
+<!--
+Tested browser versions: IE9, Firefox 4.0, Chrome 13 dev, Opera 11.10.
+
+Firefox and Chrome will replace a <blockquote> by a <p> or other given tag.  IE
+and Opera will nest the <p> inside instead.  The latter makes more sense, given
+that a) we don't support formatBlock with <blockquote> and b) <blockquote>s are
+logically different, since they can contain many lines.
+
+Firefox will not convert other tags like <p> to <div>, it will only wrap
+unwrapped lines in a <div>.  Firefox also won't replace <div> by things like
+<p>, it will nest the <p> inside.  The spec follows other browsers.
+
+If you try to convert a <dt> to a <div> or <p> or such, Firefox breaks out of
+the <dl> entirely, leaving ...<dt><br></dt></dl>.  Chrome will convert a <dt>
+or <dd> to the given element, leaving a <div> or <p> or such as the child of a
+<dl>.  I follow IE/Opera, which only affect the contents of <dt>/<dd> (Firefox
+behaves this way for <dd> as well, just not <dt>).  This means you can get
+invalid DOMs like <dt><p>foo<p></dt>, but they can be serialized as text/html,
+so I'm not too fussy.
+
+When it comes to <li>, IE/Opera behave like with <dt>/<dd>, which is how I
+behave too.  Firefox apparently refuses to do anything.  Chrome tries to wrap
+the parent list element, breaking it up if only some of the children are
+selected; this produces unserializable DOMs if you're wrapping with <p>.
+
+When you're converting multiple blocks at once, Chrome replaces them all by one
+block with <br> stuck in, like <p>foo</p><p>bar</p> -> <div>foo<br>bar</div>.
+It wipes out intervening block containers too in some cases.  This might make
+sense for <address>/<h*>/<pre>, but other browsers don't do it.
+-->
+
+<p><span>Action</span>:
+
+<ol>
+  <li>If <var>value</var> begins with a "&lt;" character and ends with a ">"
+  character, remove the first and last characters from it.
+  <!-- IE9 requires the brackets.  If they're not provided, it does nothing.
+  -->
+
+  <li>Let <var>value</var> be <span data-anolis-spec=domcore>converted to
+  lowercase</span>.
+
+  <li>If <var>value</var> is not "address", "div", "h1", "h2", "h3", "h4",
+  "h5", "h6", "p", or "pre", then do nothing and abort these steps.
+
+  <p class=XXX>Non-IE browsers support a lot more elements, but I have my
+  doubts about how much sense it makes to support things where you'd expect to
+  nest them.  Will send mail to the list.
+  <!--
+  Opera 11.10 throws NOT_SUPPORTED_ERR for bad elements, all other tested
+  browsers ignore the input.  Testing in IE9, Firefox 4.0, Chrome 13 dev, and
+  Opera 11.10, supported elements seem to be:
+
+  Everyone: address, div, h*, p, pre
+  Everyone but IE: blockquote
+  Everyone but Opera: dd, dt
+  IE only: ol, ul
+  Firefox and Chrome only: dl
+  Chrome only: article, aside, footer, header, hgroup, nav, section
+
+  HTML5 as of May 2011 supports: address, article, aside, blockquote, div,
+  footer, h*, header, hgroup, nav, p, pre, section, which exactly matches
+  Chrome except minus dd/dt/dl.
+  -->
+
+  <li><span>Block-extend</span> the <span>active range</span>, and let <var>new
+  range</var> be the result.
+
+  <li>Let <var>node list</var> be an empty list of [[nodes]].
+
+  <li>For each [[node]] <var>node</var> [[contained]] in <var>new range</var>,
+  append <var>node</var> to <var>node list</var> if it is
+  <span>editable</span>, the last member of <var>node list</var> (if any) is
+  not an [[ancestor]] of <var>node</var>, and <var>node</var> is not an
+  <span>HTML element</span> that has [[localname]] "article", "aside",
+  "blockquote", "caption", "col", "colgroup", "dd", "dl", "dt", "footer",
+  "header", "hgroup", "li", "nav", "ol", "section", "table", "tbody", "td",
+  "th", "thead", "tr", or "ul".
+
+  <li>While <var>node list</var> is not empty:
+
+  <ol>
+    <li>If the first member of <var>node list</var> is a <span>single-line
+    container</span>, <span>set the tag name</span> of the first member of
+    <var>node list</var> to <var>value</var>, then remove the first member from
+    <var>node list</var> and continue this loop from the beginning.
+
+    <div class=XXX>
+    <p>This implies that
+
+    <xmp><p>foo</p><p>bar</p></xmp>
+
+    <p>becomes (for instance)
+
+    <xmp><h1>foo</h1><h1>bar</h1></xmp>
+
+    <p>even though
+
+    <xmp><h1>foo<br>bar</h1></xmp>
+
+    <p>would probably make more sense.  WebKit has the latter behavior in all
+    cases.  Perhaps we should adopt WebKit's behavior for things where it
+    rarely makes sense to have two in a row, while keeping the current behavior
+    for divs and paragraphs?
+    </div>
+
+    <li>Let <var>sublist</var> be an empty list of [[nodes]].
+
+    <li>Remove the first member of <var>node list</var> and append it to
+    <var>sublist</var>.
+
+    <li>While <var>node list</var> is not empty, and the first member of
+    <var>node list</var> is the [[nextsibling]] of the last member of
+    <var>sublist</var>, and the first member of <var>node list</var> is not a
+    <span>single-line container</span>, and the last member of
+    <var>sublist</var> is not a [[br]], remove the first member of <var>node
+    list</var> and append it to <var>sublist</var>.
+
+    <li><span>Wrap</span> <var>sublist</var>, with <span>sibling
+    criteria</span> matching nothing and <span>new parent instructions</span>
+    returning the result of running <code data-anolis-spec=domcore
+    title=dom-Document-createElement>createElement(<var>value</var>)</code> on
+    the [[contextobject]].
+  </ol>
+</ol>
+
+
 <h3><dfn>The <code title>indent</code> command</dfn></h3>
 <!--
 IE9: Outputs <blockquote style="margin-right: 0px" dir="ltr">, or when