tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

implementation.js (329752B)


      1 "use strict";
      2 
      3 var htmlNamespace = "http://www.w3.org/1999/xhtml";
      4 
      5 var cssStylingFlag = false;
      6 
      7 var defaultSingleLineContainerName = "div";
      8 
      9 // This is bad :(
     10 var globalRange = null;
     11 
     12 // Commands are stored in a dictionary where we call their actions and such
     13 var commands = {};
     14 
     15 ///////////////////////////////////////////////////////////////////////////////
     16 ////////////////////////////// Utility functions //////////////////////////////
     17 ///////////////////////////////////////////////////////////////////////////////
     18 //@{
     19 
     20 function nextNode(node) {
     21    if (node.hasChildNodes()) {
     22        return node.firstChild;
     23    }
     24    return nextNodeDescendants(node);
     25 }
     26 
     27 function previousNode(node) {
     28    if (node.previousSibling) {
     29        node = node.previousSibling;
     30        while (node.hasChildNodes()) {
     31            node = node.lastChild;
     32        }
     33        return node;
     34    }
     35    if (node.parentNode
     36    && node.parentNode.nodeType == Node.ELEMENT_NODE) {
     37        return node.parentNode;
     38    }
     39    return null;
     40 }
     41 
     42 function nextNodeDescendants(node) {
     43    while (node && !node.nextSibling) {
     44        node = node.parentNode;
     45    }
     46    if (!node) {
     47        return null;
     48    }
     49    return node.nextSibling;
     50 }
     51 
     52 /**
     53 * Returns true if ancestor is an ancestor of descendant, false otherwise.
     54 */
     55 function isAncestor(ancestor, descendant) {
     56    return ancestor
     57        && descendant
     58        && Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY);
     59 }
     60 
     61 /**
     62 * Returns true if ancestor is an ancestor of or equal to descendant, false
     63 * otherwise.
     64 */
     65 function isAncestorContainer(ancestor, descendant) {
     66    return (ancestor || descendant)
     67        && (ancestor == descendant || isAncestor(ancestor, descendant));
     68 }
     69 
     70 /**
     71 * Returns true if descendant is a descendant of ancestor, false otherwise.
     72 */
     73 function isDescendant(descendant, ancestor) {
     74    return ancestor
     75        && descendant
     76        && Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY);
     77 }
     78 
     79 /**
     80 * Returns true if node1 is before node2 in tree order, false otherwise.
     81 */
     82 function isBefore(node1, node2) {
     83    return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION_FOLLOWING);
     84 }
     85 
     86 /**
     87 * Returns true if node1 is after node2 in tree order, false otherwise.
     88 */
     89 function isAfter(node1, node2) {
     90    return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION_PRECEDING);
     91 }
     92 
     93 function getAncestors(node) {
     94    var ancestors = [];
     95    while (node.parentNode) {
     96        ancestors.unshift(node.parentNode);
     97        node = node.parentNode;
     98    }
     99    return ancestors;
    100 }
    101 
    102 function getInclusiveAncestors(node) {
    103    return getAncestors(node).concat(node);
    104 }
    105 
    106 function getDescendants(node) {
    107    var descendants = [];
    108    var stop = nextNodeDescendants(node);
    109    while ((node = nextNode(node))
    110    && node != stop) {
    111        descendants.push(node);
    112    }
    113    return descendants;
    114 }
    115 
    116 function getInclusiveDescendants(node) {
    117    return [node].concat(getDescendants(node));
    118 }
    119 
    120 function convertProperty(property) {
    121    // Special-case for now
    122    var map = {
    123        "fontFamily": "font-family",
    124        "fontSize": "font-size",
    125        "fontStyle": "font-style",
    126        "fontWeight": "font-weight",
    127        "textDecoration": "text-decoration",
    128    };
    129    if (typeof map[property] != "undefined") {
    130        return map[property];
    131    }
    132 
    133    return property;
    134 }
    135 
    136 // Return the <font size=X> value for the given CSS size, or undefined if there
    137 // is none.
    138 function cssSizeToLegacy(cssVal) {
    139    return {
    140        "x-small": 1,
    141        "small": 2,
    142        "medium": 3,
    143        "large": 4,
    144        "x-large": 5,
    145        "xx-large": 6,
    146        "xxx-large": 7
    147    }[cssVal];
    148 }
    149 
    150 // Return the CSS size given a legacy size.
    151 function legacySizeToCss(legacyVal) {
    152    return {
    153        1: "x-small",
    154        2: "small",
    155        3: "medium",
    156        4: "large",
    157        5: "x-large",
    158        6: "xx-large",
    159        7: "xxx-large",
    160    }[legacyVal];
    161 }
    162 
    163 // Opera 11 puts HTML elements in the null namespace, it seems.
    164 function isHtmlNamespace(ns) {
    165    return ns === null
    166        || ns === htmlNamespace;
    167 }
    168 
    169 // "the directionality" from HTML.  I don't bother caring about non-HTML
    170 // elements.
    171 //
    172 // "The directionality of an element is either 'ltr' or 'rtl', and is
    173 // determined as per the first appropriate set of steps from the following
    174 // list:"
    175 function getDirectionality(element) {
    176    // "If the element's dir attribute is in the ltr state
    177    //     The directionality of the element is 'ltr'."
    178    if (element.dir == "ltr") {
    179        return "ltr";
    180    }
    181 
    182    // "If the element's dir attribute is in the rtl state
    183    //     The directionality of the element is 'rtl'."
    184    if (element.dir == "rtl") {
    185        return "rtl";
    186    }
    187 
    188    // "If the element's dir attribute is in the auto state
    189    // "If the element is a bdi element and the dir attribute is not in a
    190    // defined state (i.e. it is not present or has an invalid value)
    191    //     [lots of complicated stuff]
    192    //
    193    // Skip this, since no browser implements it anyway.
    194 
    195    // "If the element is a root element and the dir attribute is not in a
    196    // defined state (i.e. it is not present or has an invalid value)
    197    //     The directionality of the element is 'ltr'."
    198    if (!isHtmlElement(element.parentNode)) {
    199        return "ltr";
    200    }
    201 
    202    // "If the element has a parent element and the dir attribute is not in a
    203    // defined state (i.e. it is not present or has an invalid value)
    204    //     The directionality of the element is the same as the element's
    205    //     parent element's directionality."
    206    return getDirectionality(element.parentNode);
    207 }
    208 
    209 //@}
    210 
    211 ///////////////////////////////////////////////////////////////////////////////
    212 ///////////////////////////// DOM Range functions /////////////////////////////
    213 ///////////////////////////////////////////////////////////////////////////////
    214 //@{
    215 
    216 function getNodeIndex(node) {
    217    var ret = 0;
    218    while (node.previousSibling) {
    219        ret++;
    220        node = node.previousSibling;
    221    }
    222    return ret;
    223 }
    224 
    225 // "The length of a Node node is the following, depending on node:
    226 //
    227 // ProcessingInstruction
    228 // DocumentType
    229 //   Always 0.
    230 // Text
    231 // Comment
    232 //   node's length.
    233 // Any other node
    234 //   node's childNodes's length."
    235 function getNodeLength(node) {
    236    switch (node.nodeType) {
    237        case Node.PROCESSING_INSTRUCTION_NODE:
    238        case Node.DOCUMENT_TYPE_NODE:
    239            return 0;
    240 
    241        case Node.TEXT_NODE:
    242        case Node.COMMENT_NODE:
    243            return node.length;
    244 
    245        default:
    246            return node.childNodes.length;
    247    }
    248 }
    249 
    250 /**
    251 * The position of two boundary points relative to one another, as defined by
    252 * DOM Range.
    253 */
    254 function getPosition(nodeA, offsetA, nodeB, offsetB) {
    255    // "If node A is the same as node B, return equal if offset A equals offset
    256    // B, before if offset A is less than offset B, and after if offset A is
    257    // greater than offset B."
    258    if (nodeA == nodeB) {
    259        if (offsetA == offsetB) {
    260            return "equal";
    261        }
    262        if (offsetA < offsetB) {
    263            return "before";
    264        }
    265        if (offsetA > offsetB) {
    266            return "after";
    267        }
    268    }
    269 
    270    // "If node A is after node B in tree order, compute the position of (node
    271    // B, offset B) relative to (node A, offset A). If it is before, return
    272    // after. If it is after, return before."
    273    if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_FOLLOWING) {
    274        var pos = getPosition(nodeB, offsetB, nodeA, offsetA);
    275        if (pos == "before") {
    276            return "after";
    277        }
    278        if (pos == "after") {
    279            return "before";
    280        }
    281    }
    282 
    283    // "If node A is an ancestor of node B:"
    284    if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_CONTAINS) {
    285        // "Let child equal node B."
    286        var child = nodeB;
    287 
    288        // "While child is not a child of node A, set child to its parent."
    289        while (child.parentNode != nodeA) {
    290            child = child.parentNode;
    291        }
    292 
    293        // "If the index of child is less than offset A, return after."
    294        if (getNodeIndex(child) < offsetA) {
    295            return "after";
    296        }
    297    }
    298 
    299    // "Return before."
    300    return "before";
    301 }
    302 
    303 /**
    304 * Returns the furthest ancestor of a Node as defined by DOM Range.
    305 */
    306 function getFurthestAncestor(node) {
    307    var root = node;
    308    while (root.parentNode != null) {
    309        root = root.parentNode;
    310    }
    311    return root;
    312 }
    313 
    314 /**
    315 * "contained" as defined by DOM Range: "A Node node is contained in a range
    316 * range if node's furthest ancestor is the same as range's root, and (node, 0)
    317 * is after range's start, and (node, length of node) is before range's end."
    318 */
    319 function isContained(node, range) {
    320    var pos1 = getPosition(node, 0, range.startContainer, range.startOffset);
    321    var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.endOffset);
    322 
    323    return getFurthestAncestor(node) == getFurthestAncestor(range.startContainer)
    324        && pos1 == "after"
    325        && pos2 == "before";
    326 }
    327 
    328 /**
    329 * Return all nodes contained in range that the provided function returns true
    330 * for, omitting any with an ancestor already being returned.
    331 */
    332 function getContainedNodes(range, condition) {
    333    if (typeof condition == "undefined") {
    334        condition = function() { return true };
    335    }
    336    var node = range.startContainer;
    337    if (node.hasChildNodes()
    338    && range.startOffset < node.childNodes.length) {
    339        // A child is contained
    340        node = node.childNodes[range.startOffset];
    341    } else if (range.startOffset == getNodeLength(node)) {
    342        // No descendant can be contained
    343        node = nextNodeDescendants(node);
    344    } else {
    345        // No children; this node at least can't be contained
    346        node = nextNode(node);
    347    }
    348 
    349    var stop = range.endContainer;
    350    if (stop.hasChildNodes()
    351    && range.endOffset < stop.childNodes.length) {
    352        // The node after the last contained node is a child
    353        stop = stop.childNodes[range.endOffset];
    354    } else {
    355        // This node and/or some of its children might be contained
    356        stop = nextNodeDescendants(stop);
    357    }
    358 
    359    var nodeList = [];
    360    while (isBefore(node, stop)) {
    361        if (isContained(node, range)
    362        && condition(node)) {
    363            nodeList.push(node);
    364            node = nextNodeDescendants(node);
    365            continue;
    366        }
    367        node = nextNode(node);
    368    }
    369    return nodeList;
    370 }
    371 
    372 /**
    373 * As above, but includes nodes with an ancestor that's already been returned.
    374 */
    375 function getAllContainedNodes(range, condition) {
    376    if (typeof condition == "undefined") {
    377        condition = function() { return true };
    378    }
    379    var node = range.startContainer;
    380    if (node.hasChildNodes()
    381    && range.startOffset < node.childNodes.length) {
    382        // A child is contained
    383        node = node.childNodes[range.startOffset];
    384    } else if (range.startOffset == getNodeLength(node)) {
    385        // No descendant can be contained
    386        node = nextNodeDescendants(node);
    387    } else {
    388        // No children; this node at least can't be contained
    389        node = nextNode(node);
    390    }
    391 
    392    var stop = range.endContainer;
    393    if (stop.hasChildNodes()
    394    && range.endOffset < stop.childNodes.length) {
    395        // The node after the last contained node is a child
    396        stop = stop.childNodes[range.endOffset];
    397    } else {
    398        // This node and/or some of its children might be contained
    399        stop = nextNodeDescendants(stop);
    400    }
    401 
    402    var nodeList = [];
    403    while (isBefore(node, stop)) {
    404        if (isContained(node, range)
    405        && condition(node)) {
    406            nodeList.push(node);
    407        }
    408        node = nextNode(node);
    409    }
    410    return nodeList;
    411 }
    412 
    413 // Returns either null, or something of the form rgb(x, y, z), or something of
    414 // the form rgb(x, y, z, w) with w != 0.
    415 function normalizeColor(color) {
    416    if (color.toLowerCase() == "currentcolor") {
    417        return null;
    418    }
    419 
    420    if (normalizeColor.resultCache === undefined) {
    421        normalizeColor.resultCache = {};
    422    }
    423 
    424    if (normalizeColor.resultCache[color] !== undefined) {
    425        return normalizeColor.resultCache[color];
    426    }
    427 
    428    var originalColor = color;
    429 
    430    var outerSpan = document.createElement("span");
    431    document.body.appendChild(outerSpan);
    432    outerSpan.style.color = "black";
    433 
    434    var innerSpan = document.createElement("span");
    435    outerSpan.appendChild(innerSpan);
    436    innerSpan.style.color = color;
    437    color = getComputedStyle(innerSpan).color;
    438 
    439    if (color == "rgb(0, 0, 0)") {
    440        // Maybe it's really black, maybe it's invalid.
    441        outerSpan.color = "white";
    442        color = getComputedStyle(innerSpan).color;
    443        if (color != "rgb(0, 0, 0)") {
    444            return normalizeColor.resultCache[originalColor] = null;
    445        }
    446    }
    447 
    448    document.body.removeChild(outerSpan);
    449 
    450    // I rely on the fact that browsers generally provide consistent syntax for
    451    // getComputedStyle(), although it's not standardized.  There are only
    452    // three exceptions I found:
    453    if (/^rgba\([0-9]+, [0-9]+, [0-9]+, 1\)$/.test(color)) {
    454        // IE10PP2 seems to do this sometimes.
    455        return normalizeColor.resultCache[originalColor] =
    456            color.replace("rgba", "rgb").replace(", 1)", ")");
    457    }
    458    if (color == "transparent") {
    459        // IE10PP2, Firefox 7.0a2, and Opera 11.50 all return "transparent" if
    460        // the specified value is "transparent".
    461        return normalizeColor.resultCache[originalColor] =
    462            "rgba(0, 0, 0, 0)";
    463    }
    464    // Chrome 15 dev adds way too many significant figures.  This isn't a full
    465    // fix, it just fixes one case that comes up in tests.
    466    color = color.replace(/, 0.496094\)$/, ", 0.5)");
    467    return normalizeColor.resultCache[originalColor] = color;
    468 }
    469 
    470 // Returns either null, or something of the form #xxxxxx.
    471 function parseSimpleColor(color) {
    472    color = normalizeColor(color);
    473    var matches = /^rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)$/.exec(color);
    474    if (matches) {
    475        return "#"
    476            + parseInt(matches[1]).toString(16).replace(/^.$/, "0$&")
    477            + parseInt(matches[2]).toString(16).replace(/^.$/, "0$&")
    478            + parseInt(matches[3]).toString(16).replace(/^.$/, "0$&");
    479    }
    480    return null;
    481 }
    482 
    483 //@}
    484 
    485 //////////////////////////////////////////////////////////////////////////////
    486 /////////////////////////// Edit command functions ///////////////////////////
    487 //////////////////////////////////////////////////////////////////////////////
    488 
    489 /////////////////////////////////////////////////
    490 ///// Methods of the HTMLDocument interface /////
    491 /////////////////////////////////////////////////
    492 //@{
    493 
    494 var executionStackDepth = 0;
    495 
    496 // Helper function for common behavior.
    497 function editCommandMethod(command, range, callback) {
    498    // Set up our global range magic, but only if we're the outermost function
    499    if (executionStackDepth == 0 && typeof range != "undefined") {
    500        globalRange = range;
    501    } else if (executionStackDepth == 0) {
    502        globalRange = null;
    503        globalRange = getActiveRange();
    504    }
    505 
    506    executionStackDepth++;
    507    try {
    508        var ret = callback();
    509    } catch(e) {
    510        executionStackDepth--;
    511        throw e;
    512    }
    513    executionStackDepth--;
    514    return ret;
    515 }
    516 
    517 function myExecCommand(command, showUi, value, range) {
    518    // "All of these methods must treat their command argument ASCII
    519    // case-insensitively."
    520    command = command.toLowerCase();
    521 
    522    // "If only one argument was provided, let show UI be false."
    523    //
    524    // If range was passed, I can't actually detect how many args were passed
    525    // . . .
    526    if (arguments.length == 1
    527    || (arguments.length >=4 && typeof showUi == "undefined")) {
    528        showUi = false;
    529    }
    530 
    531    // "If only one or two arguments were provided, let value be the empty
    532    // string."
    533    if (arguments.length <= 2
    534    || (arguments.length >=4 && typeof value == "undefined")) {
    535        value = "";
    536    }
    537 
    538    return editCommandMethod(command, range, (function(command, showUi, value) { return function() {
    539        // "If command is not supported or not enabled, return false."
    540        if (!(command in commands) || !myQueryCommandEnabled(command)) {
    541            return false;
    542        }
    543 
    544        // "Take the action for command, passing value to the instructions as an
    545        // argument."
    546        var ret = commands[command].action(value);
    547 
    548        // Check for bugs
    549        if (ret !== true && ret !== false) {
    550            throw "execCommand() didn't return true or false: " + ret;
    551        }
    552 
    553        // "If the previous step returned false, return false."
    554        if (ret === false) {
    555            return false;
    556        }
    557 
    558        // "Return true."
    559        return true;
    560    }})(command, showUi, value));
    561 }
    562 
    563 function myQueryCommandEnabled(command, range) {
    564    // "All of these methods must treat their command argument ASCII
    565    // case-insensitively."
    566    command = command.toLowerCase();
    567 
    568    return editCommandMethod(command, range, (function(command) { return function() {
    569        // "Return true if command is both supported and enabled, false
    570        // otherwise."
    571        if (!(command in commands)) {
    572            return false;
    573        }
    574 
    575        // "Among commands defined in this specification, those listed in
    576        // Miscellaneous commands are always enabled, except for the cut
    577        // command and the paste command. The other commands defined here are
    578        // enabled if the active range is not null, its start node is either
    579        // editable or an editing host, its end node is either editable or an
    580        // editing host, and there is some editing host that is an inclusive
    581        // ancestor of both its start node and its end node."
    582        return ["copy", "defaultparagraphseparator", "selectall", "stylewithcss",
    583        "usecss"].indexOf(command) != -1
    584            || (
    585                getActiveRange() !== null
    586                && (isEditable(getActiveRange().startContainer) || isEditingHost(getActiveRange().startContainer))
    587                && (isEditable(getActiveRange().endContainer) || isEditingHost(getActiveRange().endContainer))
    588                && (getInclusiveAncestors(getActiveRange().commonAncestorContainer).some(isEditingHost))
    589            );
    590    }})(command));
    591 }
    592 
    593 function myQueryCommandIndeterm(command, range) {
    594    // "All of these methods must treat their command argument ASCII
    595    // case-insensitively."
    596    command = command.toLowerCase();
    597 
    598    return editCommandMethod(command, range, (function(command) { return function() {
    599        // "If command is not supported or has no indeterminacy, return false."
    600        if (!(command in commands) || !("indeterm" in commands[command])) {
    601            return false;
    602        }
    603 
    604        // "Return true if command is indeterminate, otherwise false."
    605        return commands[command].indeterm();
    606    }})(command));
    607 }
    608 
    609 function myQueryCommandState(command, range) {
    610    // "All of these methods must treat their command argument ASCII
    611    // case-insensitively."
    612    command = command.toLowerCase();
    613 
    614    return editCommandMethod(command, range, (function(command) { return function() {
    615        // "If command is not supported or has no state, return false."
    616        if (!(command in commands) || !("state" in commands[command])) {
    617            return false;
    618        }
    619 
    620        // "If the state override for command is set, return it."
    621        if (typeof getStateOverride(command) != "undefined") {
    622            return getStateOverride(command);
    623        }
    624 
    625        // "Return true if command's state is true, otherwise false."
    626        return commands[command].state();
    627    }})(command));
    628 }
    629 
    630 // "When the queryCommandSupported(command) method on the HTMLDocument
    631 // interface is invoked, the user agent must return true if command is
    632 // supported, and false otherwise."
    633 function myQueryCommandSupported(command) {
    634    // "All of these methods must treat their command argument ASCII
    635    // case-insensitively."
    636    command = command.toLowerCase();
    637 
    638    return command in commands;
    639 }
    640 
    641 function myQueryCommandValue(command, range) {
    642    // "All of these methods must treat their command argument ASCII
    643    // case-insensitively."
    644    command = command.toLowerCase();
    645 
    646    return editCommandMethod(command, range, function() {
    647        // "If command is not supported or has no value, return the empty string."
    648        if (!(command in commands) || !("value" in commands[command])) {
    649            return "";
    650        }
    651 
    652        // "If command is "fontSize" and its value override is set, convert the
    653        // value override to an integer number of pixels and return the legacy
    654        // font size for the result."
    655        if (command == "fontsize"
    656        && getValueOverride("fontsize") !== undefined) {
    657            return getLegacyFontSize(getValueOverride("fontsize"));
    658        }
    659 
    660        // "If the value override for command is set, return it."
    661        if (typeof getValueOverride(command) != "undefined") {
    662            return getValueOverride(command);
    663        }
    664 
    665        // "Return command's value."
    666        return commands[command].value();
    667    });
    668 }
    669 //@}
    670 
    671 //////////////////////////////
    672 ///// Common definitions /////
    673 //////////////////////////////
    674 //@{
    675 
    676 // "An HTML element is an Element whose namespace is the HTML namespace."
    677 //
    678 // I allow an extra argument to more easily check whether something is a
    679 // particular HTML element, like isHtmlElement(node, "OL").  It accepts arrays
    680 // too, like isHtmlElement(node, ["OL", "UL"]) to check if it's an ol or ul.
    681 function isHtmlElement(node, tags) {
    682    if (typeof tags == "string") {
    683        tags = [tags];
    684    }
    685    if (typeof tags == "object") {
    686        tags = tags.map(function(tag) { return tag.toUpperCase() });
    687    }
    688    return node
    689        && node.nodeType == Node.ELEMENT_NODE
    690        && isHtmlNamespace(node.namespaceURI)
    691        && (typeof tags == "undefined" || tags.indexOf(node.tagName) != -1);
    692 }
    693 
    694 // "A prohibited paragraph child name is "address", "article", "aside",
    695 // "blockquote", "caption", "center", "col", "colgroup", "dd", "details",
    696 // "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer",
    697 // "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li",
    698 // "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section",
    699 // "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul", or
    700 // "xmp"."
    701 var prohibitedParagraphChildNames = ["address", "article", "aside",
    702    "blockquote", "caption", "center", "col", "colgroup", "dd", "details",
    703    "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer",
    704    "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li",
    705    "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section",
    706    "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul",
    707    "xmp"];
    708 
    709 // "A prohibited paragraph child is an HTML element whose local name is a
    710 // prohibited paragraph child name."
    711 function isProhibitedParagraphChild(node) {
    712    return isHtmlElement(node, prohibitedParagraphChildNames);
    713 }
    714 
    715 // "A block node is either an Element whose "display" property does not have
    716 // resolved value "inline" or "inline-block" or "inline-table" or "none", or a
    717 // Document, or a DocumentFragment."
    718 function isBlockNode(node) {
    719    return node
    720        && ((node.nodeType == Node.ELEMENT_NODE && ["inline", "inline-block", "inline-table", "none"].indexOf(getComputedStyle(node).display) == -1)
    721        || node.nodeType == Node.DOCUMENT_NODE
    722        || node.nodeType == Node.DOCUMENT_FRAGMENT_NODE);
    723 }
    724 
    725 // "An inline node is a node that is not a block node."
    726 function isInlineNode(node) {
    727    return node && !isBlockNode(node);
    728 }
    729 
    730 // "An editing host is a node that is either an HTML element with a
    731 // contenteditable attribute set to the true state, or the HTML element child
    732 // of a Document whose designMode is enabled."
    733 function isEditingHost(node) {
    734    return node
    735        && isHtmlElement(node)
    736        && (node.contentEditable == "true"
    737        || (node.parentNode
    738        && node.parentNode.nodeType == Node.DOCUMENT_NODE
    739        && node.parentNode.designMode == "on"));
    740 }
    741 
    742 // "Something is editable if it is a node; it is not an editing host; it does
    743 // not have a contenteditable attribute set to the false state; its parent is
    744 // an editing host or editable; and either it is an HTML element, or it is an
    745 // svg or math element, or it is not an Element and its parent is an HTML
    746 // element."
    747 function isEditable(node) {
    748    return node
    749        && !isEditingHost(node)
    750        && (node.nodeType != Node.ELEMENT_NODE || node.contentEditable != "false")
    751        && (isEditingHost(node.parentNode) || isEditable(node.parentNode))
    752        && (isHtmlElement(node)
    753        || (node.nodeType == Node.ELEMENT_NODE && node.namespaceURI == "http://www.w3.org/2000/svg" && node.localName == "svg")
    754        || (node.nodeType == Node.ELEMENT_NODE && node.namespaceURI == "http://www.w3.org/1998/Math/MathML" && node.localName == "math")
    755        || (node.nodeType != Node.ELEMENT_NODE && isHtmlElement(node.parentNode)));
    756 }
    757 
    758 // Helper function, not defined in the spec
    759 function hasEditableDescendants(node) {
    760    for (var i = 0; i < node.childNodes.length; i++) {
    761        if (isEditable(node.childNodes[i])
    762        || hasEditableDescendants(node.childNodes[i])) {
    763            return true;
    764        }
    765    }
    766    return false;
    767 }
    768 
    769 // "The editing host of node is null if node is neither editable nor an editing
    770 // host; node itself, if node is an editing host; or the nearest ancestor of
    771 // node that is an editing host, if node is editable."
    772 function getEditingHostOf(node) {
    773    if (isEditingHost(node)) {
    774        return node;
    775    } else if (isEditable(node)) {
    776        var ancestor = node.parentNode;
    777        while (!isEditingHost(ancestor)) {
    778            ancestor = ancestor.parentNode;
    779        }
    780        return ancestor;
    781    } else {
    782        return null;
    783    }
    784 }
    785 
    786 // "Two nodes are in the same editing host if the editing host of the first is
    787 // non-null and the same as the editing host of the second."
    788 function inSameEditingHost(node1, node2) {
    789    return getEditingHostOf(node1)
    790        && getEditingHostOf(node1) == getEditingHostOf(node2);
    791 }
    792 
    793 // "A collapsed line break is a br that begins a line box which has nothing
    794 // else in it, and therefore has zero height."
    795 function isCollapsedLineBreak(br) {
    796    if (!isHtmlElement(br, "br")) {
    797        return false;
    798    }
    799 
    800    // Add a zwsp after it and see if that changes the height of the nearest
    801    // non-inline parent.  Note: this is not actually reliable, because the
    802    // parent might have a fixed height or something.
    803    var ref = br.parentNode;
    804    while (getComputedStyle(ref).display == "inline") {
    805        ref = ref.parentNode;
    806    }
    807    var refStyle = ref.hasAttribute("style") ? ref.getAttribute("style") : null;
    808    ref.style.height = "auto";
    809    ref.style.maxHeight = "none";
    810    ref.style.minHeight = "0";
    811    var space = document.createTextNode("\u200b");
    812    var origHeight = ref.offsetHeight;
    813    if (origHeight == 0) {
    814        throw "isCollapsedLineBreak: original height is zero, bug?";
    815    }
    816    br.parentNode.insertBefore(space, br.nextSibling);
    817    var finalHeight = ref.offsetHeight;
    818    space.parentNode.removeChild(space);
    819    if (refStyle === null) {
    820        // Without the setAttribute() line, removeAttribute() doesn't work in
    821        // Chrome 14 dev.  I have no idea why.
    822        ref.setAttribute("style", "");
    823        ref.removeAttribute("style");
    824    } else {
    825        ref.setAttribute("style", refStyle);
    826    }
    827 
    828    // Allow some leeway in case the zwsp didn't create a whole new line, but
    829    // only made an existing line slightly higher.  Firefox 6.0a2 shows this
    830    // behavior when the first line is bold.
    831    return origHeight < finalHeight - 5;
    832 }
    833 
    834 // "An extraneous line break is a br that has no visual effect, in that
    835 // removing it from the DOM would not change layout, except that a br that is
    836 // the sole child of an li is not extraneous."
    837 //
    838 // FIXME: This doesn't work in IE, since IE ignores display: none in
    839 // contenteditable.
    840 function isExtraneousLineBreak(br) {
    841    if (!isHtmlElement(br, "br")) {
    842        return false;
    843    }
    844 
    845    if (isHtmlElement(br.parentNode, "li")
    846    && br.parentNode.childNodes.length == 1) {
    847        return false;
    848    }
    849 
    850    // Make the line break disappear and see if that changes the block's
    851    // height.  Yes, this is an absurd hack.  We have to reset height etc. on
    852    // the reference node because otherwise its height won't change if it's not
    853    // auto.
    854    var ref = br.parentNode;
    855    while (getComputedStyle(ref).display == "inline") {
    856        ref = ref.parentNode;
    857    }
    858    var refStyle = ref.hasAttribute("style") ? ref.getAttribute("style") : null;
    859    ref.style.height = "auto";
    860    ref.style.maxHeight = "none";
    861    ref.style.minHeight = "0";
    862    var brStyle = br.hasAttribute("style") ? br.getAttribute("style") : null;
    863    var origHeight = ref.offsetHeight;
    864    if (origHeight == 0) {
    865        throw "isExtraneousLineBreak: original height is zero, bug?";
    866    }
    867    br.setAttribute("style", "display:none");
    868    var finalHeight = ref.offsetHeight;
    869    if (refStyle === null) {
    870        // Without the setAttribute() line, removeAttribute() doesn't work in
    871        // Chrome 14 dev.  I have no idea why.
    872        ref.setAttribute("style", "");
    873        ref.removeAttribute("style");
    874    } else {
    875        ref.setAttribute("style", refStyle);
    876    }
    877    if (brStyle === null) {
    878        br.removeAttribute("style");
    879    } else {
    880        br.setAttribute("style", brStyle);
    881    }
    882 
    883    return origHeight == finalHeight;
    884 }
    885 
    886 // "A whitespace node is either a Text node whose data is the empty string; or
    887 // a Text node whose data consists only of one or more tabs (0x0009), line
    888 // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose
    889 // parent is an Element whose resolved value for "white-space" is "normal" or
    890 // "nowrap"; or a Text node whose data consists only of one or more tabs
    891 // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose
    892 // parent is an Element whose resolved value for "white-space" is "pre-line"."
    893 function isWhitespaceNode(node) {
    894    return node
    895        && node.nodeType == Node.TEXT_NODE
    896        && (node.data == ""
    897        || (
    898            /^[\t\n\r ]+$/.test(node.data)
    899            && node.parentNode
    900            && node.parentNode.nodeType == Node.ELEMENT_NODE
    901            && ["normal", "nowrap"].indexOf(getComputedStyle(node.parentNode).whiteSpace) != -1
    902        ) || (
    903            /^[\t\r ]+$/.test(node.data)
    904            && node.parentNode
    905            && node.parentNode.nodeType == Node.ELEMENT_NODE
    906            && getComputedStyle(node.parentNode).whiteSpace == "pre-line"
    907        ));
    908 }
    909 
    910 // "node is a collapsed whitespace node if the following algorithm returns
    911 // true:"
    912 function isCollapsedWhitespaceNode(node) {
    913    // "If node is not a whitespace node, return false."
    914    if (!isWhitespaceNode(node)) {
    915        return false;
    916    }
    917 
    918    // "If node's data is the empty string, return true."
    919    if (node.data == "") {
    920        return true;
    921    }
    922 
    923    // "Let ancestor be node's parent."
    924    var ancestor = node.parentNode;
    925 
    926    // "If ancestor is null, return true."
    927    if (!ancestor) {
    928        return true;
    929    }
    930 
    931    // "If the "display" property of some ancestor of node has resolved value
    932    // "none", return true."
    933    if (getAncestors(node).some(function(ancestor) {
    934        return ancestor.nodeType == Node.ELEMENT_NODE
    935            && getComputedStyle(ancestor).display == "none";
    936    })) {
    937        return true;
    938    }
    939 
    940    // "While ancestor is not a block node and its parent is not null, set
    941    // ancestor to its parent."
    942    while (!isBlockNode(ancestor)
    943    && ancestor.parentNode) {
    944        ancestor = ancestor.parentNode;
    945    }
    946 
    947    // "Let reference be node."
    948    var reference = node;
    949 
    950    // "While reference is a descendant of ancestor:"
    951    while (reference != ancestor) {
    952        // "Let reference be the node before it in tree order."
    953        reference = previousNode(reference);
    954 
    955        // "If reference is a block node or a br, return true."
    956        if (isBlockNode(reference)
    957        || isHtmlElement(reference, "br")) {
    958            return true;
    959        }
    960 
    961        // "If reference is a Text node that is not a whitespace node, or is an
    962        // img, break from this loop."
    963        if ((reference.nodeType == Node.TEXT_NODE && !isWhitespaceNode(reference))
    964        || isHtmlElement(reference, "img")) {
    965            break;
    966        }
    967    }
    968 
    969    // "Let reference be node."
    970    reference = node;
    971 
    972    // "While reference is a descendant of ancestor:"
    973    var stop = nextNodeDescendants(ancestor);
    974    while (reference != stop) {
    975        // "Let reference be the node after it in tree order, or null if there
    976        // is no such node."
    977        reference = nextNode(reference);
    978 
    979        // "If reference is a block node or a br, return true."
    980        if (isBlockNode(reference)
    981        || isHtmlElement(reference, "br")) {
    982            return true;
    983        }
    984 
    985        // "If reference is a Text node that is not a whitespace node, or is an
    986        // img, break from this loop."
    987        if ((reference && reference.nodeType == Node.TEXT_NODE && !isWhitespaceNode(reference))
    988        || isHtmlElement(reference, "img")) {
    989            break;
    990        }
    991    }
    992 
    993    // "Return false."
    994    return false;
    995 }
    996 
    997 // "Something is visible if it is a node that either is a block node, or a Text
    998 // node that is not a collapsed whitespace node, or an img, or a br that is not
    999 // an extraneous line break, or any node with a visible descendant; excluding
   1000 // any node with an ancestor container Element whose "display" property has
   1001 // resolved value "none"."
   1002 function isVisible(node) {
   1003    if (!node) {
   1004        return false;
   1005    }
   1006 
   1007    if (getAncestors(node).concat(node)
   1008    .filter(function(node) { return node.nodeType == Node.ELEMENT_NODE })
   1009    .some(function(node) { return getComputedStyle(node).display == "none" })) {
   1010        return false;
   1011    }
   1012 
   1013    if (isBlockNode(node)
   1014    || (node.nodeType == Node.TEXT_NODE && !isCollapsedWhitespaceNode(node))
   1015    || isHtmlElement(node, "img")
   1016    || (isHtmlElement(node, "br") && !isExtraneousLineBreak(node))) {
   1017        return true;
   1018    }
   1019 
   1020    for (var i = 0; i < node.childNodes.length; i++) {
   1021        if (isVisible(node.childNodes[i])) {
   1022            return true;
   1023        }
   1024    }
   1025 
   1026    return false;
   1027 }
   1028 
   1029 // "Something is invisible if it is a node that is not visible."
   1030 function isInvisible(node) {
   1031    return node && !isVisible(node);
   1032 }
   1033 
   1034 // "A collapsed block prop is either a collapsed line break that is not an
   1035 // extraneous line break, or an Element that is an inline node and whose
   1036 // children are all either invisible or collapsed block props and that has at
   1037 // least one child that is a collapsed block prop."
   1038 function isCollapsedBlockProp(node) {
   1039    if (isCollapsedLineBreak(node)
   1040    && !isExtraneousLineBreak(node)) {
   1041        return true;
   1042    }
   1043 
   1044    if (!isInlineNode(node)
   1045    || node.nodeType != Node.ELEMENT_NODE) {
   1046        return false;
   1047    }
   1048 
   1049    var hasCollapsedBlockPropChild = false;
   1050    for (var i = 0; i < node.childNodes.length; i++) {
   1051        if (!isInvisible(node.childNodes[i])
   1052        && !isCollapsedBlockProp(node.childNodes[i])) {
   1053            return false;
   1054        }
   1055        if (isCollapsedBlockProp(node.childNodes[i])) {
   1056            hasCollapsedBlockPropChild = true;
   1057        }
   1058    }
   1059 
   1060    return hasCollapsedBlockPropChild;
   1061 }
   1062 
   1063 // "The active range is the range of the selection given by calling
   1064 // getSelection() on the context object. (Thus the active range may be null.)"
   1065 //
   1066 // We cheat and return globalRange if that's defined.  We also ensure that the
   1067 // active range meets the requirements that selection boundary points are
   1068 // supposed to meet, i.e., that the nodes are both Text or Element nodes that
   1069 // descend from a Document.
   1070 function getActiveRange() {
   1071    var ret;
   1072    if (globalRange) {
   1073        ret = globalRange;
   1074    } else if (getSelection().rangeCount) {
   1075        ret = getSelection().getRangeAt(0);
   1076    } else {
   1077        return null;
   1078    }
   1079    if ([Node.TEXT_NODE, Node.ELEMENT_NODE].indexOf(ret.startContainer.nodeType) == -1
   1080    || [Node.TEXT_NODE, Node.ELEMENT_NODE].indexOf(ret.endContainer.nodeType) == -1
   1081    || !ret.startContainer.ownerDocument
   1082    || !ret.endContainer.ownerDocument
   1083    || !isDescendant(ret.startContainer, ret.startContainer.ownerDocument)
   1084    || !isDescendant(ret.endContainer, ret.endContainer.ownerDocument)) {
   1085        throw "Invalid active range; test bug?";
   1086    }
   1087    return ret;
   1088 }
   1089 
   1090 // "For some commands, each HTMLDocument must have a boolean state override
   1091 // and/or a string value override. These do not change the command's state or
   1092 // value, but change the way some algorithms behave, as specified in those
   1093 // algorithms' definitions. Initially, both must be unset for every command.
   1094 // Whenever the number of ranges in the Selection changes to something
   1095 // different, and whenever a boundary point of the range at a given index in
   1096 // the Selection changes to something different, the state override and value
   1097 // override must be unset for every command."
   1098 //
   1099 // We implement this crudely by using setters and getters.  To verify that the
   1100 // selection hasn't changed, we copy the active range and just check the
   1101 // endpoints match.  This isn't really correct, but it's good enough for us.
   1102 // Unset state/value overrides are undefined.  We put everything in a function
   1103 // so no one can access anything except via the provided functions, since
   1104 // otherwise callers might mistakenly use outdated overrides (if the selection
   1105 // has changed).
   1106 var getStateOverride, setStateOverride, unsetStateOverride,
   1107    getValueOverride, setValueOverride, unsetValueOverride;
   1108 (function() {
   1109    var stateOverrides = {};
   1110    var valueOverrides = {};
   1111    var storedRange = null;
   1112 
   1113    function resetOverrides() {
   1114        if (!storedRange
   1115        || storedRange.startContainer != getActiveRange().startContainer
   1116        || storedRange.endContainer != getActiveRange().endContainer
   1117        || storedRange.startOffset != getActiveRange().startOffset
   1118        || storedRange.endOffset != getActiveRange().endOffset) {
   1119            stateOverrides = {};
   1120            valueOverrides = {};
   1121            storedRange = getActiveRange().cloneRange();
   1122        }
   1123    }
   1124 
   1125    getStateOverride = function(command) {
   1126        resetOverrides();
   1127        return stateOverrides[command];
   1128    };
   1129 
   1130    setStateOverride = function(command, newState) {
   1131        resetOverrides();
   1132        stateOverrides[command] = newState;
   1133    };
   1134 
   1135    unsetStateOverride = function(command) {
   1136        resetOverrides();
   1137        delete stateOverrides[command];
   1138    }
   1139 
   1140    getValueOverride = function(command) {
   1141        resetOverrides();
   1142        return valueOverrides[command];
   1143    }
   1144 
   1145    // "The value override for the backColor command must be the same as the
   1146    // value override for the hiliteColor command, such that setting one sets
   1147    // the other to the same thing and unsetting one unsets the other."
   1148    setValueOverride = function(command, newValue) {
   1149        resetOverrides();
   1150        valueOverrides[command] = newValue;
   1151        if (command == "backcolor") {
   1152            valueOverrides.hilitecolor = newValue;
   1153        } else if (command == "hilitecolor") {
   1154            valueOverrides.backcolor = newValue;
   1155        }
   1156    }
   1157 
   1158    unsetValueOverride = function(command) {
   1159        resetOverrides();
   1160        delete valueOverrides[command];
   1161        if (command == "backcolor") {
   1162            delete valueOverrides.hilitecolor;
   1163        } else if (command == "hilitecolor") {
   1164            delete valueOverrides.backcolor;
   1165        }
   1166    }
   1167 })();
   1168 
   1169 //@}
   1170 
   1171 /////////////////////////////
   1172 ///// Common algorithms /////
   1173 /////////////////////////////
   1174 
   1175 ///// Assorted common algorithms /////
   1176 //@{
   1177 
   1178 // Magic array of extra ranges whose endpoints we want to preserve.
   1179 var extraRanges = [];
   1180 
   1181 function movePreservingRanges(node, newParent, newIndex) {
   1182    // For convenience, I allow newIndex to be -1 to mean "insert at the end".
   1183    if (newIndex == -1) {
   1184        newIndex = newParent.childNodes.length;
   1185    }
   1186 
   1187    // "When the user agent is to move a Node to a new location, preserving
   1188    // ranges, it must remove the Node from its original parent (if any), then
   1189    // insert it in the new location. In doing so, however, it must ignore the
   1190    // regular range mutation rules, and instead follow these rules:"
   1191 
   1192    // "Let node be the moved Node, old parent and old index be the old parent
   1193    // (which may be null) and index, and new parent and new index be the new
   1194    // parent and index."
   1195    var oldParent = node.parentNode;
   1196    var oldIndex = getNodeIndex(node);
   1197 
   1198    // We preserve the global range object, the ranges in the selection, and
   1199    // any range that's in the extraRanges array.  Any other ranges won't get
   1200    // updated, because we have no references to them.
   1201    var ranges = [globalRange].concat(extraRanges);
   1202    for (var i = 0; i < getSelection().rangeCount; i++) {
   1203        ranges.push(getSelection().getRangeAt(i));
   1204    }
   1205    var boundaryPoints = [];
   1206    ranges.forEach(function(range) {
   1207        boundaryPoints.push([range.startContainer, range.startOffset]);
   1208        boundaryPoints.push([range.endContainer, range.endOffset]);
   1209    });
   1210 
   1211    boundaryPoints.forEach(function(boundaryPoint) {
   1212        // "If a boundary point's node is the same as or a descendant of node,
   1213        // leave it unchanged, so it moves to the new location."
   1214        //
   1215        // No modifications necessary.
   1216 
   1217        // "If a boundary point's node is new parent and its offset is greater
   1218        // than new index, add one to its offset."
   1219        if (boundaryPoint[0] == newParent
   1220        && boundaryPoint[1] > newIndex) {
   1221            boundaryPoint[1]++;
   1222        }
   1223 
   1224        // "If a boundary point's node is old parent and its offset is old index or
   1225        // old index + 1, set its node to new parent and add new index − old index
   1226        // to its offset."
   1227        if (boundaryPoint[0] == oldParent
   1228        && (boundaryPoint[1] == oldIndex
   1229        || boundaryPoint[1] == oldIndex + 1)) {
   1230            boundaryPoint[0] = newParent;
   1231            boundaryPoint[1] += newIndex - oldIndex;
   1232        }
   1233 
   1234        // "If a boundary point's node is old parent and its offset is greater than
   1235        // old index + 1, subtract one from its offset."
   1236        if (boundaryPoint[0] == oldParent
   1237        && boundaryPoint[1] > oldIndex + 1) {
   1238            boundaryPoint[1]--;
   1239        }
   1240    });
   1241 
   1242    // Now actually move it and preserve the ranges.
   1243    if (newParent.childNodes.length == newIndex) {
   1244        newParent.appendChild(node);
   1245    } else {
   1246        newParent.insertBefore(node, newParent.childNodes[newIndex]);
   1247    }
   1248 
   1249    globalRange.setStart(boundaryPoints[0][0], boundaryPoints[0][1]);
   1250    globalRange.setEnd(boundaryPoints[1][0], boundaryPoints[1][1]);
   1251 
   1252    for (var i = 0; i < extraRanges.length; i++) {
   1253        extraRanges[i].setStart(boundaryPoints[2*i + 2][0], boundaryPoints[2*i + 2][1]);
   1254        extraRanges[i].setEnd(boundaryPoints[2*i + 3][0], boundaryPoints[2*i + 3][1]);
   1255    }
   1256 
   1257    getSelection().removeAllRanges();
   1258    for (var i = 1 + extraRanges.length; i < ranges.length; i++) {
   1259        var newRange = document.createRange();
   1260        newRange.setStart(boundaryPoints[2*i][0], boundaryPoints[2*i][1]);
   1261        newRange.setEnd(boundaryPoints[2*i + 1][0], boundaryPoints[2*i + 1][1]);
   1262        getSelection().addRange(newRange);
   1263    }
   1264 }
   1265 
   1266 function setTagName(element, newName) {
   1267    // "If element is an HTML element with local name equal to new name, return
   1268    // element."
   1269    if (isHtmlElement(element, newName.toUpperCase())) {
   1270        return element;
   1271    }
   1272 
   1273    // "If element's parent is null, return element."
   1274    if (!element.parentNode) {
   1275        return element;
   1276    }
   1277 
   1278    // "Let replacement element be the result of calling createElement(new
   1279    // name) on the ownerDocument of element."
   1280    var replacementElement = element.ownerDocument.createElement(newName);
   1281 
   1282    // "Insert replacement element into element's parent immediately before
   1283    // element."
   1284    element.parentNode.insertBefore(replacementElement, element);
   1285 
   1286    // "Copy all attributes of element to replacement element, in order."
   1287    for (var i = 0; i < element.attributes.length; i++) {
   1288        replacementElement.setAttributeNS(element.attributes[i].namespaceURI, element.attributes[i].name, element.attributes[i].value);
   1289    }
   1290 
   1291    // "While element has children, append the first child of element as the
   1292    // last child of replacement element, preserving ranges."
   1293    while (element.childNodes.length) {
   1294        movePreservingRanges(element.firstChild, replacementElement, replacementElement.childNodes.length);
   1295    }
   1296 
   1297    // "Remove element from its parent."
   1298    element.parentNode.removeChild(element);
   1299 
   1300    // "Return replacement element."
   1301    return replacementElement;
   1302 }
   1303 
   1304 function removeExtraneousLineBreaksBefore(node) {
   1305    // "Let ref be the previousSibling of node."
   1306    var ref = node.previousSibling;
   1307 
   1308    // "If ref is null, abort these steps."
   1309    if (!ref) {
   1310        return;
   1311    }
   1312 
   1313    // "While ref has children, set ref to its lastChild."
   1314    while (ref.hasChildNodes()) {
   1315        ref = ref.lastChild;
   1316    }
   1317 
   1318    // "While ref is invisible but not an extraneous line break, and ref does
   1319    // not equal node's parent, set ref to the node before it in tree order."
   1320    while (isInvisible(ref)
   1321    && !isExtraneousLineBreak(ref)
   1322    && ref != node.parentNode) {
   1323        ref = previousNode(ref);
   1324    }
   1325 
   1326    // "If ref is an editable extraneous line break, remove it from its
   1327    // parent."
   1328    if (isEditable(ref)
   1329    && isExtraneousLineBreak(ref)) {
   1330        ref.parentNode.removeChild(ref);
   1331    }
   1332 }
   1333 
   1334 function removeExtraneousLineBreaksAtTheEndOf(node) {
   1335    // "Let ref be node."
   1336    var ref = node;
   1337 
   1338    // "While ref has children, set ref to its lastChild."
   1339    while (ref.hasChildNodes()) {
   1340        ref = ref.lastChild;
   1341    }
   1342 
   1343    // "While ref is invisible but not an extraneous line break, and ref does
   1344    // not equal node, set ref to the node before it in tree order."
   1345    while (isInvisible(ref)
   1346    && !isExtraneousLineBreak(ref)
   1347    && ref != node) {
   1348        ref = previousNode(ref);
   1349    }
   1350 
   1351    // "If ref is an editable extraneous line break:"
   1352    if (isEditable(ref)
   1353    && isExtraneousLineBreak(ref)) {
   1354        // "While ref's parent is editable and invisible, set ref to its
   1355        // parent."
   1356        while (isEditable(ref.parentNode)
   1357        && isInvisible(ref.parentNode)) {
   1358            ref = ref.parentNode;
   1359        }
   1360 
   1361        // "Remove ref from its parent."
   1362        ref.parentNode.removeChild(ref);
   1363    }
   1364 }
   1365 
   1366 // "To remove extraneous line breaks from a node, first remove extraneous line
   1367 // breaks before it, then remove extraneous line breaks at the end of it."
   1368 function removeExtraneousLineBreaksFrom(node) {
   1369    removeExtraneousLineBreaksBefore(node);
   1370    removeExtraneousLineBreaksAtTheEndOf(node);
   1371 }
   1372 
   1373 //@}
   1374 ///// Wrapping a list of nodes /////
   1375 //@{
   1376 
   1377 function wrap(nodeList, siblingCriteria, newParentInstructions) {
   1378    // "If not provided, sibling criteria returns false and new parent
   1379    // instructions returns null."
   1380    if (typeof siblingCriteria == "undefined") {
   1381        siblingCriteria = function() { return false };
   1382    }
   1383    if (typeof newParentInstructions == "undefined") {
   1384        newParentInstructions = function() { return null };
   1385    }
   1386 
   1387    // "If every member of node list is invisible, and none is a br, return
   1388    // null and abort these steps."
   1389    if (nodeList.every(isInvisible)
   1390    && !nodeList.some(function(node) { return isHtmlElement(node, "br") })) {
   1391        return null;
   1392    }
   1393 
   1394    // "If node list's first member's parent is null, return null and abort
   1395    // these steps."
   1396    if (!nodeList[0].parentNode) {
   1397        return null;
   1398    }
   1399 
   1400    // "If node list's last member is an inline node that's not a br, and node
   1401    // list's last member's nextSibling is a br, append that br to node list."
   1402    if (isInlineNode(nodeList[nodeList.length - 1])
   1403    && !isHtmlElement(nodeList[nodeList.length - 1], "br")
   1404    && isHtmlElement(nodeList[nodeList.length - 1].nextSibling, "br")) {
   1405        nodeList.push(nodeList[nodeList.length - 1].nextSibling);
   1406    }
   1407 
   1408    // "While node list's first member's previousSibling is invisible, prepend
   1409    // it to node list."
   1410    while (isInvisible(nodeList[0].previousSibling)) {
   1411        nodeList.unshift(nodeList[0].previousSibling);
   1412    }
   1413 
   1414    // "While node list's last member's nextSibling is invisible, append it to
   1415    // node list."
   1416    while (isInvisible(nodeList[nodeList.length - 1].nextSibling)) {
   1417        nodeList.push(nodeList[nodeList.length - 1].nextSibling);
   1418    }
   1419 
   1420    // "If the previousSibling of the first member of node list is editable and
   1421    // running sibling criteria on it returns true, let new parent be the
   1422    // previousSibling of the first member of node list."
   1423    var newParent;
   1424    if (isEditable(nodeList[0].previousSibling)
   1425    && siblingCriteria(nodeList[0].previousSibling)) {
   1426        newParent = nodeList[0].previousSibling;
   1427 
   1428    // "Otherwise, if the nextSibling of the last member of node list is
   1429    // editable and running sibling criteria on it returns true, let new parent
   1430    // be the nextSibling of the last member of node list."
   1431    } else if (isEditable(nodeList[nodeList.length - 1].nextSibling)
   1432    && siblingCriteria(nodeList[nodeList.length - 1].nextSibling)) {
   1433        newParent = nodeList[nodeList.length - 1].nextSibling;
   1434 
   1435    // "Otherwise, run new parent instructions, and let new parent be the
   1436    // result."
   1437    } else {
   1438        newParent = newParentInstructions();
   1439    }
   1440 
   1441    // "If new parent is null, abort these steps and return null."
   1442    if (!newParent) {
   1443        return null;
   1444    }
   1445 
   1446    // "If new parent's parent is null:"
   1447    if (!newParent.parentNode) {
   1448        // "Insert new parent into the parent of the first member of node list
   1449        // immediately before the first member of node list."
   1450        nodeList[0].parentNode.insertBefore(newParent, nodeList[0]);
   1451 
   1452        // "If any range has a boundary point with node equal to the parent of
   1453        // new parent and offset equal to the index of new parent, add one to
   1454        // that boundary point's offset."
   1455        //
   1456        // Only try to fix the global range.
   1457        if (globalRange.startContainer == newParent.parentNode
   1458        && globalRange.startOffset == getNodeIndex(newParent)) {
   1459            globalRange.setStart(globalRange.startContainer, globalRange.startOffset + 1);
   1460        }
   1461        if (globalRange.endContainer == newParent.parentNode
   1462        && globalRange.endOffset == getNodeIndex(newParent)) {
   1463            globalRange.setEnd(globalRange.endContainer, globalRange.endOffset + 1);
   1464        }
   1465    }
   1466 
   1467    // "Let original parent be the parent of the first member of node list."
   1468    var originalParent = nodeList[0].parentNode;
   1469 
   1470    // "If new parent is before the first member of node list in tree order:"
   1471    if (isBefore(newParent, nodeList[0])) {
   1472        // "If new parent is not an inline node, but the last visible child of
   1473        // new parent and the first visible member of node list are both inline
   1474        // nodes, and the last child of new parent is not a br, call
   1475        // createElement("br") on the ownerDocument of new parent and append
   1476        // the result as the last child of new parent."
   1477        if (!isInlineNode(newParent)
   1478        && isInlineNode([].filter.call(newParent.childNodes, isVisible).slice(-1)[0])
   1479        && isInlineNode(nodeList.filter(isVisible)[0])
   1480        && !isHtmlElement(newParent.lastChild, "BR")) {
   1481            newParent.appendChild(newParent.ownerDocument.createElement("br"));
   1482        }
   1483 
   1484        // "For each node in node list, append node as the last child of new
   1485        // parent, preserving ranges."
   1486        for (var i = 0; i < nodeList.length; i++) {
   1487            movePreservingRanges(nodeList[i], newParent, -1);
   1488        }
   1489 
   1490    // "Otherwise:"
   1491    } else {
   1492        // "If new parent is not an inline node, but the first visible child of
   1493        // new parent and the last visible member of node list are both inline
   1494        // nodes, and the last member of node list is not a br, call
   1495        // createElement("br") on the ownerDocument of new parent and insert
   1496        // the result as the first child of new parent."
   1497        if (!isInlineNode(newParent)
   1498        && isInlineNode([].filter.call(newParent.childNodes, isVisible)[0])
   1499        && isInlineNode(nodeList.filter(isVisible).slice(-1)[0])
   1500        && !isHtmlElement(nodeList[nodeList.length - 1], "BR")) {
   1501            newParent.insertBefore(newParent.ownerDocument.createElement("br"), newParent.firstChild);
   1502        }
   1503 
   1504        // "For each node in node list, in reverse order, insert node as the
   1505        // first child of new parent, preserving ranges."
   1506        for (var i = nodeList.length - 1; i >= 0; i--) {
   1507            movePreservingRanges(nodeList[i], newParent, 0);
   1508        }
   1509    }
   1510 
   1511    // "If original parent is editable and has no children, remove it from its
   1512    // parent."
   1513    if (isEditable(originalParent) && !originalParent.hasChildNodes()) {
   1514        originalParent.parentNode.removeChild(originalParent);
   1515    }
   1516 
   1517    // "If new parent's nextSibling is editable and running sibling criteria on
   1518    // it returns true:"
   1519    if (isEditable(newParent.nextSibling)
   1520    && siblingCriteria(newParent.nextSibling)) {
   1521        // "If new parent is not an inline node, but new parent's last child
   1522        // and new parent's nextSibling's first child are both inline nodes,
   1523        // and new parent's last child is not a br, call createElement("br") on
   1524        // the ownerDocument of new parent and append the result as the last
   1525        // child of new parent."
   1526        if (!isInlineNode(newParent)
   1527        && isInlineNode(newParent.lastChild)
   1528        && isInlineNode(newParent.nextSibling.firstChild)
   1529        && !isHtmlElement(newParent.lastChild, "BR")) {
   1530            newParent.appendChild(newParent.ownerDocument.createElement("br"));
   1531        }
   1532 
   1533        // "While new parent's nextSibling has children, append its first child
   1534        // as the last child of new parent, preserving ranges."
   1535        while (newParent.nextSibling.hasChildNodes()) {
   1536            movePreservingRanges(newParent.nextSibling.firstChild, newParent, -1);
   1537        }
   1538 
   1539        // "Remove new parent's nextSibling from its parent."
   1540        newParent.parentNode.removeChild(newParent.nextSibling);
   1541    }
   1542 
   1543    // "Remove extraneous line breaks from new parent."
   1544    removeExtraneousLineBreaksFrom(newParent);
   1545 
   1546    // "Return new parent."
   1547    return newParent;
   1548 }
   1549 
   1550 
   1551 //@}
   1552 ///// Allowed children /////
   1553 //@{
   1554 
   1555 // "A name of an element with inline contents is "a", "abbr", "b", "bdi",
   1556 // "bdo", "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i",
   1557 // "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small",
   1558 // "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike",
   1559 // "xmp", "big", "blink", "font", "marquee", "nobr", or "tt"."
   1560 var namesOfElementsWithInlineContents = ["a", "abbr", "b", "bdi", "bdo",
   1561    "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i",
   1562    "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small",
   1563    "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike",
   1564    "xmp", "big", "blink", "font", "marquee", "nobr", "tt"];
   1565 
   1566 // "An element with inline contents is an HTML element whose local name is a
   1567 // name of an element with inline contents."
   1568 function isElementWithInlineContents(node) {
   1569    return isHtmlElement(node, namesOfElementsWithInlineContents);
   1570 }
   1571 
   1572 function isAllowedChild(child, parent_) {
   1573    // "If parent is "colgroup", "table", "tbody", "tfoot", "thead", "tr", or
   1574    // an HTML element with local name equal to one of those, and child is a
   1575    // Text node whose data does not consist solely of space characters, return
   1576    // false."
   1577    if ((["colgroup", "table", "tbody", "tfoot", "thead", "tr"].indexOf(parent_) != -1
   1578    || isHtmlElement(parent_, ["colgroup", "table", "tbody", "tfoot", "thead", "tr"]))
   1579    && typeof child == "object"
   1580    && child.nodeType == Node.TEXT_NODE
   1581    && !/^[ \t\n\f\r]*$/.test(child.data)) {
   1582        return false;
   1583    }
   1584 
   1585    // "If parent is "script", "style", "plaintext", or "xmp", or an HTML
   1586    // element with local name equal to one of those, and child is not a Text
   1587    // node, return false."
   1588    if ((["script", "style", "plaintext", "xmp"].indexOf(parent_) != -1
   1589    || isHtmlElement(parent_, ["script", "style", "plaintext", "xmp"]))
   1590    && (typeof child != "object" || child.nodeType != Node.TEXT_NODE)) {
   1591        return false;
   1592    }
   1593 
   1594    // "If child is a Document, DocumentFragment, or DocumentType, return
   1595    // false."
   1596    if (typeof child == "object"
   1597    && (child.nodeType == Node.DOCUMENT_NODE
   1598    || child.nodeType == Node.DOCUMENT_FRAGMENT_NODE
   1599    || child.nodeType == Node.DOCUMENT_TYPE_NODE)) {
   1600        return false;
   1601    }
   1602 
   1603    // "If child is an HTML element, set child to the local name of child."
   1604    if (isHtmlElement(child)) {
   1605        child = child.tagName.toLowerCase();
   1606    }
   1607 
   1608    // "If child is not a string, return true."
   1609    if (typeof child != "string") {
   1610        return true;
   1611    }
   1612 
   1613    // "If parent is an HTML element:"
   1614    if (isHtmlElement(parent_)) {
   1615        // "If child is "a", and parent or some ancestor of parent is an a,
   1616        // return false."
   1617        //
   1618        // "If child is a prohibited paragraph child name and parent or some
   1619        // ancestor of parent is an element with inline contents, return
   1620        // false."
   1621        //
   1622        // "If child is "h1", "h2", "h3", "h4", "h5", or "h6", and parent or
   1623        // some ancestor of parent is an HTML element with local name "h1",
   1624        // "h2", "h3", "h4", "h5", or "h6", return false."
   1625        var ancestor = parent_;
   1626        while (ancestor) {
   1627            if (child == "a" && isHtmlElement(ancestor, "a")) {
   1628                return false;
   1629            }
   1630            if (prohibitedParagraphChildNames.indexOf(child) != -1
   1631            && isElementWithInlineContents(ancestor)) {
   1632                return false;
   1633            }
   1634            if (/^h[1-6]$/.test(child)
   1635            && isHtmlElement(ancestor)
   1636            && /^H[1-6]$/.test(ancestor.tagName)) {
   1637                return false;
   1638            }
   1639            ancestor = ancestor.parentNode;
   1640        }
   1641 
   1642        // "Let parent be the local name of parent."
   1643        parent_ = parent_.tagName.toLowerCase();
   1644    }
   1645 
   1646    // "If parent is an Element or DocumentFragment, return true."
   1647    if (typeof parent_ == "object"
   1648    && (parent_.nodeType == Node.ELEMENT_NODE
   1649    || parent_.nodeType == Node.DOCUMENT_FRAGMENT_NODE)) {
   1650        return true;
   1651    }
   1652 
   1653    // "If parent is not a string, return false."
   1654    if (typeof parent_ != "string") {
   1655        return false;
   1656    }
   1657 
   1658    // "If parent is on the left-hand side of an entry on the following list,
   1659    // then return true if child is listed on the right-hand side of that
   1660    // entry, and false otherwise."
   1661    switch (parent_) {
   1662        case "colgroup":
   1663            return child == "col";
   1664        case "table":
   1665            return ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"].indexOf(child) != -1;
   1666        case "tbody":
   1667        case "thead":
   1668        case "tfoot":
   1669            return ["td", "th", "tr"].indexOf(child) != -1;
   1670        case "tr":
   1671            return ["td", "th"].indexOf(child) != -1;
   1672        case "dl":
   1673            return ["dt", "dd"].indexOf(child) != -1;
   1674        case "dir":
   1675        case "ol":
   1676        case "ul":
   1677            return ["dir", "li", "ol", "ul"].indexOf(child) != -1;
   1678        case "hgroup":
   1679            return /^h[1-6]$/.test(child);
   1680    }
   1681 
   1682    // "If child is "body", "caption", "col", "colgroup", "frame", "frameset",
   1683    // "head", "html", "tbody", "td", "tfoot", "th", "thead", or "tr", return
   1684    // false."
   1685    if (["body", "caption", "col", "colgroup", "frame", "frameset", "head",
   1686    "html", "tbody", "td", "tfoot", "th", "thead", "tr"].indexOf(child) != -1) {
   1687        return false;
   1688    }
   1689 
   1690    // "If child is "dd" or "dt" and parent is not "dl", return false."
   1691    if (["dd", "dt"].indexOf(child) != -1
   1692    && parent_ != "dl") {
   1693        return false;
   1694    }
   1695 
   1696    // "If child is "li" and parent is not "ol" or "ul", return false."
   1697    if (child == "li"
   1698    && parent_ != "ol"
   1699    && parent_ != "ul") {
   1700        return false;
   1701    }
   1702 
   1703    // "If parent is on the left-hand side of an entry on the following list
   1704    // and child is listed on the right-hand side of that entry, return false."
   1705    var table = [
   1706        [["a"], ["a"]],
   1707        [["dd", "dt"], ["dd", "dt"]],
   1708        [["h1", "h2", "h3", "h4", "h5", "h6"], ["h1", "h2", "h3", "h4", "h5", "h6"]],
   1709        [["li"], ["li"]],
   1710        [["nobr"], ["nobr"]],
   1711        [namesOfElementsWithInlineContents, prohibitedParagraphChildNames],
   1712        [["td", "th"], ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"]],
   1713    ];
   1714    for (var i = 0; i < table.length; i++) {
   1715        if (table[i][0].indexOf(parent_) != -1
   1716        && table[i][1].indexOf(child) != -1) {
   1717            return false;
   1718        }
   1719    }
   1720 
   1721    // "Return true."
   1722    return true;
   1723 }
   1724 
   1725 
   1726 //@}
   1727 
   1728 //////////////////////////////////////
   1729 ///// Inline formatting commands /////
   1730 //////////////////////////////////////
   1731 
   1732 ///// Inline formatting command definitions /////
   1733 //@{
   1734 
   1735 // "A node node is effectively contained in a range range if range is not
   1736 // collapsed, and at least one of the following holds:"
   1737 function isEffectivelyContained(node, range) {
   1738    if (range.collapsed) {
   1739        return false;
   1740    }
   1741 
   1742    // "node is contained in range."
   1743    if (isContained(node, range)) {
   1744        return true;
   1745    }
   1746 
   1747    // "node is range's start node, it is a Text node, and its length is
   1748    // different from range's start offset."
   1749    if (node == range.startContainer
   1750    && node.nodeType == Node.TEXT_NODE
   1751    && getNodeLength(node) != range.startOffset) {
   1752        return true;
   1753    }
   1754 
   1755    // "node is range's end node, it is a Text node, and range's end offset is
   1756    // not 0."
   1757    if (node == range.endContainer
   1758    && node.nodeType == Node.TEXT_NODE
   1759    && range.endOffset != 0) {
   1760        return true;
   1761    }
   1762 
   1763    // "node has at least one child; and all its children are effectively
   1764    // contained in range; and either range's start node is not a descendant of
   1765    // node or is not a Text node or range's start offset is zero; and either
   1766    // range's end node is not a descendant of node or is not a Text node or
   1767    // range's end offset is its end node's length."
   1768    if (node.hasChildNodes()
   1769    && [].every.call(node.childNodes, function(child) { return isEffectivelyContained(child, range) })
   1770    && (!isDescendant(range.startContainer, node)
   1771    || range.startContainer.nodeType != Node.TEXT_NODE
   1772    || range.startOffset == 0)
   1773    && (!isDescendant(range.endContainer, node)
   1774    || range.endContainer.nodeType != Node.TEXT_NODE
   1775    || range.endOffset == getNodeLength(range.endContainer))) {
   1776        return true;
   1777    }
   1778 
   1779    return false;
   1780 }
   1781 
   1782 // Like get(All)ContainedNodes(), but for effectively contained nodes.
   1783 function getEffectivelyContainedNodes(range, condition) {
   1784    if (typeof condition == "undefined") {
   1785        condition = function() { return true };
   1786    }
   1787    var node = range.startContainer;
   1788    while (isEffectivelyContained(node.parentNode, range)) {
   1789        node = node.parentNode;
   1790    }
   1791 
   1792    var stop = nextNodeDescendants(range.endContainer);
   1793 
   1794    var nodeList = [];
   1795    while (isBefore(node, stop)) {
   1796        if (isEffectivelyContained(node, range)
   1797        && condition(node)) {
   1798            nodeList.push(node);
   1799            node = nextNodeDescendants(node);
   1800            continue;
   1801        }
   1802        node = nextNode(node);
   1803    }
   1804    return nodeList;
   1805 }
   1806 
   1807 function getAllEffectivelyContainedNodes(range, condition) {
   1808    if (typeof condition == "undefined") {
   1809        condition = function() { return true };
   1810    }
   1811    var node = range.startContainer;
   1812    while (isEffectivelyContained(node.parentNode, range)) {
   1813        node = node.parentNode;
   1814    }
   1815 
   1816    var stop = nextNodeDescendants(range.endContainer);
   1817 
   1818    var nodeList = [];
   1819    while (isBefore(node, stop)) {
   1820        if (isEffectivelyContained(node, range)
   1821        && condition(node)) {
   1822            nodeList.push(node);
   1823        }
   1824        node = nextNode(node);
   1825    }
   1826    return nodeList;
   1827 }
   1828 
   1829 // "A modifiable element is a b, em, i, s, span, strong, sub, sup, or u element
   1830 // with no attributes except possibly style; or a font element with no
   1831 // attributes except possibly style, color, face, and/or size; or an a element
   1832 // with no attributes except possibly style and/or href."
   1833 function isModifiableElement(node) {
   1834    if (!isHtmlElement(node)) {
   1835        return false;
   1836    }
   1837 
   1838    if (["B", "EM", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"].indexOf(node.tagName) != -1) {
   1839        if (node.attributes.length == 0) {
   1840            return true;
   1841        }
   1842 
   1843        if (node.attributes.length == 1
   1844        && node.hasAttribute("style")) {
   1845            return true;
   1846        }
   1847    }
   1848 
   1849    if (node.tagName == "FONT" || node.tagName == "A") {
   1850        var numAttrs = node.attributes.length;
   1851 
   1852        if (node.hasAttribute("style")) {
   1853            numAttrs--;
   1854        }
   1855 
   1856        if (node.tagName == "FONT") {
   1857            if (node.hasAttribute("color")) {
   1858                numAttrs--;
   1859            }
   1860 
   1861            if (node.hasAttribute("face")) {
   1862                numAttrs--;
   1863            }
   1864 
   1865            if (node.hasAttribute("size")) {
   1866                numAttrs--;
   1867            }
   1868        }
   1869 
   1870        if (node.tagName == "A"
   1871        && node.hasAttribute("href")) {
   1872            numAttrs--;
   1873        }
   1874 
   1875        if (numAttrs == 0) {
   1876            return true;
   1877        }
   1878    }
   1879 
   1880    return false;
   1881 }
   1882 
   1883 function isSimpleModifiableElement(node) {
   1884    // "A simple modifiable element is an HTML element for which at least one
   1885    // of the following holds:"
   1886    if (!isHtmlElement(node)) {
   1887        return false;
   1888    }
   1889 
   1890    // Only these elements can possibly be a simple modifiable element.
   1891    if (["A", "B", "EM", "FONT", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"].indexOf(node.tagName) == -1) {
   1892        return false;
   1893    }
   1894 
   1895    // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u
   1896    // element with no attributes."
   1897    if (node.attributes.length == 0) {
   1898        return true;
   1899    }
   1900 
   1901    // If it's got more than one attribute, everything after this fails.
   1902    if (node.attributes.length > 1) {
   1903        return false;
   1904    }
   1905 
   1906    // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u
   1907    // element with exactly one attribute, which is style, which sets no CSS
   1908    // properties (including invalid or unrecognized properties)."
   1909    //
   1910    // Not gonna try for invalid or unrecognized.
   1911    if (node.hasAttribute("style")
   1912    && node.style.length == 0) {
   1913        return true;
   1914    }
   1915 
   1916    // "It is an a element with exactly one attribute, which is href."
   1917    if (node.tagName == "A"
   1918    && node.hasAttribute("href")) {
   1919        return true;
   1920    }
   1921 
   1922    // "It is a font element with exactly one attribute, which is either color,
   1923    // face, or size."
   1924    if (node.tagName == "FONT"
   1925    && (node.hasAttribute("color")
   1926    || node.hasAttribute("face")
   1927    || node.hasAttribute("size")
   1928    )) {
   1929        return true;
   1930    }
   1931 
   1932    // "It is a b or strong element with exactly one attribute, which is style,
   1933    // and the style attribute sets exactly one CSS property (including invalid
   1934    // or unrecognized properties), which is "font-weight"."
   1935    if ((node.tagName == "B" || node.tagName == "STRONG")
   1936    && node.hasAttribute("style")
   1937    && node.style.length == 1
   1938    && node.style.fontWeight != "") {
   1939        return true;
   1940    }
   1941 
   1942    // "It is an i or em element with exactly one attribute, which is style,
   1943    // and the style attribute sets exactly one CSS property (including invalid
   1944    // or unrecognized properties), which is "font-style"."
   1945    if ((node.tagName == "I" || node.tagName == "EM")
   1946    && node.hasAttribute("style")
   1947    && node.style.length == 1
   1948    && node.style.fontStyle != "") {
   1949        return true;
   1950    }
   1951 
   1952    // "It is an a, font, or span element with exactly one attribute, which is
   1953    // style, and the style attribute sets exactly one CSS property (including
   1954    // invalid or unrecognized properties), and that property is not
   1955    // "text-decoration"."
   1956    if ((node.tagName == "A" || node.tagName == "FONT" || node.tagName == "SPAN")
   1957    && node.hasAttribute("style")
   1958    && node.style.length == 1
   1959    && node.style.textDecoration == "") {
   1960        return true;
   1961    }
   1962 
   1963    // "It is an a, font, s, span, strike, or u element with exactly one
   1964    // attribute, which is style, and the style attribute sets exactly one CSS
   1965    // property (including invalid or unrecognized properties), which is
   1966    // "text-decoration", which is set to "line-through" or "underline" or
   1967    // "overline" or "none"."
   1968    //
   1969    // The weird extra node.style.length check is for Firefox, which as of
   1970    // 8.0a2 has annoying and weird behavior here.
   1971    if (["A", "FONT", "S", "SPAN", "STRIKE", "U"].indexOf(node.tagName) != -1
   1972    && node.hasAttribute("style")
   1973    && (node.style.length == 1
   1974        || (node.style.length == 4
   1975            && "MozTextBlink" in node.style
   1976            && "MozTextDecorationColor" in node.style
   1977            && "MozTextDecorationLine" in node.style
   1978            && "MozTextDecorationStyle" in node.style)
   1979        || (node.style.length == 4
   1980            && "MozTextBlink" in node.style
   1981            && "textDecorationColor" in node.style
   1982            && "textDecorationLine" in node.style
   1983            && "textDecorationStyle" in node.style)
   1984    )
   1985    && (node.style.textDecoration == "line-through"
   1986    || node.style.textDecoration == "underline"
   1987    || node.style.textDecoration == "overline"
   1988    || node.style.textDecoration == "none")) {
   1989        return true;
   1990    }
   1991 
   1992    return false;
   1993 }
   1994 
   1995 // "A formattable node is an editable visible node that is either a Text node,
   1996 // an img, or a br."
   1997 function isFormattableNode(node) {
   1998    return isEditable(node)
   1999        && isVisible(node)
   2000        && (node.nodeType == Node.TEXT_NODE
   2001        || isHtmlElement(node, ["img", "br"]));
   2002 }
   2003 
   2004 // "Two quantities are equivalent values for a command if either both are null,
   2005 // or both are strings and they're equal and the command does not define any
   2006 // equivalent values, or both are strings and the command defines equivalent
   2007 // values and they match the definition."
   2008 function areEquivalentValues(command, val1, val2) {
   2009    if (val1 === null && val2 === null) {
   2010        return true;
   2011    }
   2012 
   2013    if (typeof val1 == "string"
   2014    && typeof val2 == "string"
   2015    && val1 == val2
   2016    && !("equivalentValues" in commands[command])) {
   2017        return true;
   2018    }
   2019 
   2020    if (typeof val1 == "string"
   2021    && typeof val2 == "string"
   2022    && "equivalentValues" in commands[command]
   2023    && commands[command].equivalentValues(val1, val2)) {
   2024        return true;
   2025    }
   2026 
   2027    return false;
   2028 }
   2029 
   2030 // "Two quantities are loosely equivalent values for a command if either they
   2031 // are equivalent values for the command, or if the command is the fontSize
   2032 // command; one of the quantities is one of "x-small", "small", "medium",
   2033 // "large", "x-large", "xx-large", or "xxx-large"; and the other quantity is
   2034 // the resolved value of "font-size" on a font element whose size attribute has
   2035 // the corresponding value set ("1" through "7" respectively)."
   2036 function areLooselyEquivalentValues(command, val1, val2) {
   2037    if (areEquivalentValues(command, val1, val2)) {
   2038        return true;
   2039    }
   2040 
   2041    if (command != "fontsize"
   2042    || typeof val1 != "string"
   2043    || typeof val2 != "string") {
   2044        return false;
   2045    }
   2046 
   2047    // Static variables in JavaScript?
   2048    var callee = areLooselyEquivalentValues;
   2049    if (callee.sizeMap === undefined) {
   2050        callee.sizeMap = {};
   2051        var font = document.createElement("font");
   2052        document.body.appendChild(font);
   2053        ["x-small", "small", "medium", "large", "x-large", "xx-large",
   2054        "xxx-large"].forEach(function(keyword) {
   2055            font.size = cssSizeToLegacy(keyword);
   2056            callee.sizeMap[keyword] = getComputedStyle(font).fontSize;
   2057        });
   2058        document.body.removeChild(font);
   2059    }
   2060 
   2061    return val1 === callee.sizeMap[val2]
   2062        || val2 === callee.sizeMap[val1];
   2063 }
   2064 
   2065 //@}
   2066 ///// Assorted inline formatting command algorithms /////
   2067 //@{
   2068 
   2069 function getEffectiveCommandValue(node, command) {
   2070    // "If neither node nor its parent is an Element, return null."
   2071    if (node.nodeType != Node.ELEMENT_NODE
   2072    && (!node.parentNode || node.parentNode.nodeType != Node.ELEMENT_NODE)) {
   2073        return null;
   2074    }
   2075 
   2076    // "If node is not an Element, return the effective command value of its
   2077    // parent for command."
   2078    if (node.nodeType != Node.ELEMENT_NODE) {
   2079        return getEffectiveCommandValue(node.parentNode, command);
   2080    }
   2081 
   2082    // "If command is "createLink" or "unlink":"
   2083    if (command == "createlink" || command == "unlink") {
   2084        // "While node is not null, and is not an a element that has an href
   2085        // attribute, set node to its parent."
   2086        while (node
   2087        && (!isHtmlElement(node)
   2088        || node.tagName != "A"
   2089        || !node.hasAttribute("href"))) {
   2090            node = node.parentNode;
   2091        }
   2092 
   2093        // "If node is null, return null."
   2094        if (!node) {
   2095            return null;
   2096        }
   2097 
   2098        // "Return the value of node's href attribute."
   2099        return node.getAttribute("href");
   2100    }
   2101 
   2102    // "If command is "backColor" or "hiliteColor":"
   2103    if (command == "backcolor"
   2104    || command == "hilitecolor") {
   2105        // "While the resolved value of "background-color" on node is any
   2106        // fully transparent value, and node's parent is an Element, set
   2107        // node to its parent."
   2108        //
   2109        // Another lame hack to avoid flawed APIs.
   2110        while ((getComputedStyle(node).backgroundColor == "rgba(0, 0, 0, 0)"
   2111        || getComputedStyle(node).backgroundColor === ""
   2112        || getComputedStyle(node).backgroundColor == "transparent")
   2113        && node.parentNode
   2114        && node.parentNode.nodeType == Node.ELEMENT_NODE) {
   2115            node = node.parentNode;
   2116        }
   2117 
   2118        // "Return the resolved value of "background-color" for node."
   2119        return getComputedStyle(node).backgroundColor;
   2120    }
   2121 
   2122    // "If command is "subscript" or "superscript":"
   2123    if (command == "subscript" || command == "superscript") {
   2124        // "Let affected by subscript and affected by superscript be two
   2125        // boolean variables, both initially false."
   2126        var affectedBySubscript = false;
   2127        var affectedBySuperscript = false;
   2128 
   2129        // "While node is an inline node:"
   2130        while (isInlineNode(node)) {
   2131            var verticalAlign = getComputedStyle(node).verticalAlign;
   2132 
   2133            // "If node is a sub, set affected by subscript to true."
   2134            if (isHtmlElement(node, "sub")) {
   2135                affectedBySubscript = true;
   2136            // "Otherwise, if node is a sup, set affected by superscript to
   2137            // true."
   2138            } else if (isHtmlElement(node, "sup")) {
   2139                affectedBySuperscript = true;
   2140            }
   2141 
   2142            // "Set node to its parent."
   2143            node = node.parentNode;
   2144        }
   2145 
   2146        // "If affected by subscript and affected by superscript are both true,
   2147        // return the string "mixed"."
   2148        if (affectedBySubscript && affectedBySuperscript) {
   2149            return "mixed";
   2150        }
   2151 
   2152        // "If affected by subscript is true, return "subscript"."
   2153        if (affectedBySubscript) {
   2154            return "subscript";
   2155        }
   2156 
   2157        // "If affected by superscript is true, return "superscript"."
   2158        if (affectedBySuperscript) {
   2159            return "superscript";
   2160        }
   2161 
   2162        // "Return null."
   2163        return null;
   2164    }
   2165 
   2166    // "If command is "strikethrough", and the "text-decoration" property of
   2167    // node or any of its ancestors has resolved value containing
   2168    // "line-through", return "line-through". Otherwise, return null."
   2169    if (command == "strikethrough") {
   2170        do {
   2171            if (getComputedStyle(node).textDecoration.indexOf("line-through") != -1) {
   2172                return "line-through";
   2173            }
   2174            node = node.parentNode;
   2175        } while (node && node.nodeType == Node.ELEMENT_NODE);
   2176        return null;
   2177    }
   2178 
   2179    // "If command is "underline", and the "text-decoration" property of node
   2180    // or any of its ancestors has resolved value containing "underline",
   2181    // return "underline". Otherwise, return null."
   2182    if (command == "underline") {
   2183        do {
   2184            if (getComputedStyle(node).textDecoration.indexOf("underline") != -1) {
   2185                return "underline";
   2186            }
   2187            node = node.parentNode;
   2188        } while (node && node.nodeType == Node.ELEMENT_NODE);
   2189        return null;
   2190    }
   2191 
   2192    if (!("relevantCssProperty" in commands[command])) {
   2193        throw "Bug: no relevantCssProperty for " + command + " in getEffectiveCommandValue";
   2194    }
   2195 
   2196    // "Return the resolved value for node of the relevant CSS property for
   2197    // command."
   2198    return getComputedStyle(node)[commands[command].relevantCssProperty];
   2199 }
   2200 
   2201 function getSpecifiedCommandValue(element, command) {
   2202    // "If command is "backColor" or "hiliteColor" and element's display
   2203    // property does not have resolved value "inline", return null."
   2204    if ((command == "backcolor" || command == "hilitecolor")
   2205    && getComputedStyle(element).display != "inline") {
   2206        return null;
   2207    }
   2208 
   2209    // "If command is "createLink" or "unlink":"
   2210    if (command == "createlink" || command == "unlink") {
   2211        // "If element is an a element and has an href attribute, return the
   2212        // value of that attribute."
   2213        if (isHtmlElement(element)
   2214        && element.tagName == "A"
   2215        && element.hasAttribute("href")) {
   2216            return element.getAttribute("href");
   2217        }
   2218 
   2219        // "Return null."
   2220        return null;
   2221    }
   2222 
   2223    // "If command is "subscript" or "superscript":"
   2224    if (command == "subscript" || command == "superscript") {
   2225        // "If element is a sup, return "superscript"."
   2226        if (isHtmlElement(element, "sup")) {
   2227            return "superscript";
   2228        }
   2229 
   2230        // "If element is a sub, return "subscript"."
   2231        if (isHtmlElement(element, "sub")) {
   2232            return "subscript";
   2233        }
   2234 
   2235        // "Return null."
   2236        return null;
   2237    }
   2238 
   2239    // "If command is "strikethrough", and element has a style attribute set,
   2240    // and that attribute sets "text-decoration":"
   2241    if (command == "strikethrough"
   2242    && element.style.textDecoration != "") {
   2243        // "If element's style attribute sets "text-decoration" to a value
   2244        // containing "line-through", return "line-through"."
   2245        if (element.style.textDecoration.indexOf("line-through") != -1) {
   2246            return "line-through";
   2247        }
   2248 
   2249        // "Return null."
   2250        return null;
   2251    }
   2252 
   2253    // "If command is "strikethrough" and element is a s or strike element,
   2254    // return "line-through"."
   2255    if (command == "strikethrough"
   2256    && isHtmlElement(element, ["S", "STRIKE"])) {
   2257        return "line-through";
   2258    }
   2259 
   2260    // "If command is "underline", and element has a style attribute set, and
   2261    // that attribute sets "text-decoration":"
   2262    if (command == "underline"
   2263    && element.style.textDecoration != "") {
   2264        // "If element's style attribute sets "text-decoration" to a value
   2265        // containing "underline", return "underline"."
   2266        if (element.style.textDecoration.indexOf("underline") != -1) {
   2267            return "underline";
   2268        }
   2269 
   2270        // "Return null."
   2271        return null;
   2272    }
   2273 
   2274    // "If command is "underline" and element is a u element, return
   2275    // "underline"."
   2276    if (command == "underline"
   2277    && isHtmlElement(element, "U")) {
   2278        return "underline";
   2279    }
   2280 
   2281    // "Let property be the relevant CSS property for command."
   2282    var property = commands[command].relevantCssProperty;
   2283 
   2284    // "If property is null, return null."
   2285    if (property === null) {
   2286        return null;
   2287    }
   2288 
   2289    // "If element has a style attribute set, and that attribute has the
   2290    // effect of setting property, return the value that it sets property to."
   2291    if (element.style[property] != "") {
   2292        return element.style[property];
   2293    }
   2294 
   2295    // "If element is a font element that has an attribute whose effect is
   2296    // to create a presentational hint for property, return the value that the
   2297    // hint sets property to.  (For a size of 7, this will be the non-CSS value
   2298    // "xxx-large".)"
   2299    if (isHtmlNamespace(element.namespaceURI)
   2300    && element.tagName == "FONT") {
   2301        if (property == "color" && element.hasAttribute("color")) {
   2302            return element.color;
   2303        }
   2304        if (property == "fontFamily" && element.hasAttribute("face")) {
   2305            return element.face;
   2306        }
   2307        if (property == "fontSize" && element.hasAttribute("size")) {
   2308            // This is not even close to correct in general.
   2309            var size = parseInt(element.size);
   2310            if (size < 1) {
   2311                size = 1;
   2312            }
   2313            if (size > 7) {
   2314                size = 7;
   2315            }
   2316            return {
   2317                1: "x-small",
   2318                2: "small",
   2319                3: "medium",
   2320                4: "large",
   2321                5: "x-large",
   2322                6: "xx-large",
   2323                7: "xxx-large"
   2324            }[size];
   2325        }
   2326    }
   2327 
   2328    // "If element is in the following list, and property is equal to the
   2329    // CSS property name listed for it, return the string listed for it."
   2330    //
   2331    // A list follows, whose meaning is copied here.
   2332    if (property == "fontWeight"
   2333    && (element.tagName == "B" || element.tagName == "STRONG")) {
   2334        return "bold";
   2335    }
   2336    if (property == "fontStyle"
   2337    && (element.tagName == "I" || element.tagName == "EM")) {
   2338        return "italic";
   2339    }
   2340 
   2341    // "Return null."
   2342    return null;
   2343 }
   2344 
   2345 function reorderModifiableDescendants(node, command, newValue) {
   2346    // "Let candidate equal node."
   2347    var candidate = node;
   2348 
   2349    // "While candidate is a modifiable element, and candidate has exactly one
   2350    // child, and that child is also a modifiable element, and candidate is not
   2351    // a simple modifiable element or candidate's specified command value for
   2352    // command is not equivalent to new value, set candidate to its child."
   2353    while (isModifiableElement(candidate)
   2354    && candidate.childNodes.length == 1
   2355    && isModifiableElement(candidate.firstChild)
   2356    && (!isSimpleModifiableElement(candidate)
   2357    || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue))) {
   2358        candidate = candidate.firstChild;
   2359    }
   2360 
   2361    // "If candidate is node, or is not a simple modifiable element, or its
   2362    // specified command value is not equivalent to new value, or its effective
   2363    // command value is not loosely equivalent to new value, abort these
   2364    // steps."
   2365    if (candidate == node
   2366    || !isSimpleModifiableElement(candidate)
   2367    || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue)
   2368    || !areLooselyEquivalentValues(command, getEffectiveCommandValue(candidate, command), newValue)) {
   2369        return;
   2370    }
   2371 
   2372    // "While candidate has children, insert the first child of candidate into
   2373    // candidate's parent immediately before candidate, preserving ranges."
   2374    while (candidate.hasChildNodes()) {
   2375        movePreservingRanges(candidate.firstChild, candidate.parentNode, getNodeIndex(candidate));
   2376    }
   2377 
   2378    // "Insert candidate into node's parent immediately after node."
   2379    node.parentNode.insertBefore(candidate, node.nextSibling);
   2380 
   2381    // "Append the node as the last child of candidate, preserving ranges."
   2382    movePreservingRanges(node, candidate, -1);
   2383 }
   2384 
   2385 function recordValues(nodeList) {
   2386    // "Let values be a list of (node, command, specified command value)
   2387    // triples, initially empty."
   2388    var values = [];
   2389 
   2390    // "For each node in node list, for each command in the list "subscript",
   2391    // "bold", "fontName", "fontSize", "foreColor", "hiliteColor", "italic",
   2392    // "strikethrough", and "underline" in that order:"
   2393    nodeList.forEach(function(node) {
   2394        ["subscript", "bold", "fontname", "fontsize", "forecolor",
   2395        "hilitecolor", "italic", "strikethrough", "underline"].forEach(function(command) {
   2396            // "Let ancestor equal node."
   2397            var ancestor = node;
   2398 
   2399            // "If ancestor is not an Element, set it to its parent."
   2400            if (ancestor.nodeType != Node.ELEMENT_NODE) {
   2401                ancestor = ancestor.parentNode;
   2402            }
   2403 
   2404            // "While ancestor is an Element and its specified command value
   2405            // for command is null, set it to its parent."
   2406            while (ancestor
   2407            && ancestor.nodeType == Node.ELEMENT_NODE
   2408            && getSpecifiedCommandValue(ancestor, command) === null) {
   2409                ancestor = ancestor.parentNode;
   2410            }
   2411 
   2412            // "If ancestor is an Element, add (node, command, ancestor's
   2413            // specified command value for command) to values. Otherwise add
   2414            // (node, command, null) to values."
   2415            if (ancestor && ancestor.nodeType == Node.ELEMENT_NODE) {
   2416                values.push([node, command, getSpecifiedCommandValue(ancestor, command)]);
   2417            } else {
   2418                values.push([node, command, null]);
   2419            }
   2420        });
   2421    });
   2422 
   2423    // "Return values."
   2424    return values;
   2425 }
   2426 
   2427 function restoreValues(values) {
   2428    // "For each (node, command, value) triple in values:"
   2429    values.forEach(function(triple) {
   2430        var node = triple[0];
   2431        var command = triple[1];
   2432        var value = triple[2];
   2433 
   2434        // "Let ancestor equal node."
   2435        var ancestor = node;
   2436 
   2437        // "If ancestor is not an Element, set it to its parent."
   2438        if (!ancestor || ancestor.nodeType != Node.ELEMENT_NODE) {
   2439            ancestor = ancestor.parentNode;
   2440        }
   2441 
   2442        // "While ancestor is an Element and its specified command value for
   2443        // command is null, set it to its parent."
   2444        while (ancestor
   2445        && ancestor.nodeType == Node.ELEMENT_NODE
   2446        && getSpecifiedCommandValue(ancestor, command) === null) {
   2447            ancestor = ancestor.parentNode;
   2448        }
   2449 
   2450        // "If value is null and ancestor is an Element, push down values on
   2451        // node for command, with new value null."
   2452        if (value === null
   2453        && ancestor
   2454        && ancestor.nodeType == Node.ELEMENT_NODE) {
   2455            pushDownValues(node, command, null);
   2456 
   2457        // "Otherwise, if ancestor is an Element and its specified command
   2458        // value for command is not equivalent to value, or if ancestor is not
   2459        // an Element and value is not null, force the value of command to
   2460        // value on node."
   2461        } else if ((ancestor
   2462        && ancestor.nodeType == Node.ELEMENT_NODE
   2463        && !areEquivalentValues(command, getSpecifiedCommandValue(ancestor, command), value))
   2464        || ((!ancestor || ancestor.nodeType != Node.ELEMENT_NODE)
   2465        && value !== null)) {
   2466            forceValue(node, command, value);
   2467        }
   2468    });
   2469 }
   2470 
   2471 
   2472 //@}
   2473 ///// Clearing an element's value /////
   2474 //@{
   2475 
   2476 function clearValue(element, command) {
   2477    // "If element is not editable, return the empty list."
   2478    if (!isEditable(element)) {
   2479        return [];
   2480    }
   2481 
   2482    // "If element's specified command value for command is null, return the
   2483    // empty list."
   2484    if (getSpecifiedCommandValue(element, command) === null) {
   2485        return [];
   2486    }
   2487 
   2488    // "If element is a simple modifiable element:"
   2489    if (isSimpleModifiableElement(element)) {
   2490        // "Let children be the children of element."
   2491        var children = Array.prototype.slice.call(element.childNodes);
   2492 
   2493        // "For each child in children, insert child into element's parent
   2494        // immediately before element, preserving ranges."
   2495        for (var i = 0; i < children.length; i++) {
   2496            movePreservingRanges(children[i], element.parentNode, getNodeIndex(element));
   2497        }
   2498 
   2499        // "Remove element from its parent."
   2500        element.parentNode.removeChild(element);
   2501 
   2502        // "Return children."
   2503        return children;
   2504    }
   2505 
   2506    // "If command is "strikethrough", and element has a style attribute that
   2507    // sets "text-decoration" to some value containing "line-through", delete
   2508    // "line-through" from the value."
   2509    if (command == "strikethrough"
   2510    && element.style.textDecoration.indexOf("line-through") != -1) {
   2511        if (element.style.textDecoration == "line-through") {
   2512            element.style.textDecoration = "";
   2513        } else {
   2514            element.style.textDecoration = element.style.textDecoration.replace("line-through", "");
   2515        }
   2516        if (element.getAttribute("style") == "") {
   2517            element.removeAttribute("style");
   2518        }
   2519    }
   2520 
   2521    // "If command is "underline", and element has a style attribute that sets
   2522    // "text-decoration" to some value containing "underline", delete
   2523    // "underline" from the value."
   2524    if (command == "underline"
   2525    && element.style.textDecoration.indexOf("underline") != -1) {
   2526        if (element.style.textDecoration == "underline") {
   2527            element.style.textDecoration = "";
   2528        } else {
   2529            element.style.textDecoration = element.style.textDecoration.replace("underline", "");
   2530        }
   2531        if (element.getAttribute("style") == "") {
   2532            element.removeAttribute("style");
   2533        }
   2534    }
   2535 
   2536    // "If the relevant CSS property for command is not null, unset the CSS
   2537    // property property of element."
   2538    if (commands[command].relevantCssProperty !== null) {
   2539        element.style[commands[command].relevantCssProperty] = '';
   2540        if (element.getAttribute("style") == "") {
   2541            element.removeAttribute("style");
   2542        }
   2543    }
   2544 
   2545    // "If element is a font element:"
   2546    if (isHtmlNamespace(element.namespaceURI) && element.tagName == "FONT") {
   2547        // "If command is "foreColor", unset element's color attribute, if set."
   2548        if (command == "forecolor") {
   2549            element.removeAttribute("color");
   2550        }
   2551 
   2552        // "If command is "fontName", unset element's face attribute, if set."
   2553        if (command == "fontname") {
   2554            element.removeAttribute("face");
   2555        }
   2556 
   2557        // "If command is "fontSize", unset element's size attribute, if set."
   2558        if (command == "fontsize") {
   2559            element.removeAttribute("size");
   2560        }
   2561    }
   2562 
   2563    // "If element is an a element and command is "createLink" or "unlink",
   2564    // unset the href property of element."
   2565    if (isHtmlElement(element, "A")
   2566    && (command == "createlink" || command == "unlink")) {
   2567        element.removeAttribute("href");
   2568    }
   2569 
   2570    // "If element's specified command value for command is null, return the
   2571    // empty list."
   2572    if (getSpecifiedCommandValue(element, command) === null) {
   2573        return [];
   2574    }
   2575 
   2576    // "Set the tag name of element to "span", and return the one-node list
   2577    // consisting of the result."
   2578    return [setTagName(element, "span")];
   2579 }
   2580 
   2581 
   2582 //@}
   2583 ///// Pushing down values /////
   2584 //@{
   2585 
   2586 function pushDownValues(node, command, newValue) {
   2587    // "If node's parent is not an Element, abort this algorithm."
   2588    if (!node.parentNode
   2589    || node.parentNode.nodeType != Node.ELEMENT_NODE) {
   2590        return;
   2591    }
   2592 
   2593    // "If the effective command value of command is loosely equivalent to new
   2594    // value on node, abort this algorithm."
   2595    if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
   2596        return;
   2597    }
   2598 
   2599    // "Let current ancestor be node's parent."
   2600    var currentAncestor = node.parentNode;
   2601 
   2602    // "Let ancestor list be a list of Nodes, initially empty."
   2603    var ancestorList = [];
   2604 
   2605    // "While current ancestor is an editable Element and the effective command
   2606    // value of command is not loosely equivalent to new value on it, append
   2607    // current ancestor to ancestor list, then set current ancestor to its
   2608    // parent."
   2609    while (isEditable(currentAncestor)
   2610    && currentAncestor.nodeType == Node.ELEMENT_NODE
   2611    && !areLooselyEquivalentValues(command, getEffectiveCommandValue(currentAncestor, command), newValue)) {
   2612        ancestorList.push(currentAncestor);
   2613        currentAncestor = currentAncestor.parentNode;
   2614    }
   2615 
   2616    // "If ancestor list is empty, abort this algorithm."
   2617    if (!ancestorList.length) {
   2618        return;
   2619    }
   2620 
   2621    // "Let propagated value be the specified command value of command on the
   2622    // last member of ancestor list."
   2623    var propagatedValue = getSpecifiedCommandValue(ancestorList[ancestorList.length - 1], command);
   2624 
   2625    // "If propagated value is null and is not equal to new value, abort this
   2626    // algorithm."
   2627    if (propagatedValue === null && propagatedValue != newValue) {
   2628        return;
   2629    }
   2630 
   2631    // "If the effective command value for the parent of the last member of
   2632    // ancestor list is not loosely equivalent to new value, and new value is
   2633    // not null, abort this algorithm."
   2634    if (newValue !== null
   2635    && !areLooselyEquivalentValues(command, getEffectiveCommandValue(ancestorList[ancestorList.length - 1].parentNode, command), newValue)) {
   2636        return;
   2637    }
   2638 
   2639    // "While ancestor list is not empty:"
   2640    while (ancestorList.length) {
   2641        // "Let current ancestor be the last member of ancestor list."
   2642        // "Remove the last member from ancestor list."
   2643        var currentAncestor = ancestorList.pop();
   2644 
   2645        // "If the specified command value of current ancestor for command is
   2646        // not null, set propagated value to that value."
   2647        if (getSpecifiedCommandValue(currentAncestor, command) !== null) {
   2648            propagatedValue = getSpecifiedCommandValue(currentAncestor, command);
   2649        }
   2650 
   2651        // "Let children be the children of current ancestor."
   2652        var children = Array.prototype.slice.call(currentAncestor.childNodes);
   2653 
   2654        // "If the specified command value of current ancestor for command is
   2655        // not null, clear the value of current ancestor."
   2656        if (getSpecifiedCommandValue(currentAncestor, command) !== null) {
   2657            clearValue(currentAncestor, command);
   2658        }
   2659 
   2660        // "For every child in children:"
   2661        for (var i = 0; i < children.length; i++) {
   2662            var child = children[i];
   2663 
   2664            // "If child is node, continue with the next child."
   2665            if (child == node) {
   2666                continue;
   2667            }
   2668 
   2669            // "If child is an Element whose specified command value for
   2670            // command is neither null nor equivalent to propagated value,
   2671            // continue with the next child."
   2672            if (child.nodeType == Node.ELEMENT_NODE
   2673            && getSpecifiedCommandValue(child, command) !== null
   2674            && !areEquivalentValues(command, propagatedValue, getSpecifiedCommandValue(child, command))) {
   2675                continue;
   2676            }
   2677 
   2678            // "If child is the last member of ancestor list, continue with the
   2679            // next child."
   2680            if (child == ancestorList[ancestorList.length - 1]) {
   2681                continue;
   2682            }
   2683 
   2684            // "Force the value of child, with command as in this algorithm
   2685            // and new value equal to propagated value."
   2686            forceValue(child, command, propagatedValue);
   2687        }
   2688    }
   2689 }
   2690 
   2691 
   2692 //@}
   2693 ///// Forcing the value of a node /////
   2694 //@{
   2695 
   2696 function forceValue(node, command, newValue) {
   2697    // "If node's parent is null, abort this algorithm."
   2698    if (!node.parentNode) {
   2699        return;
   2700    }
   2701 
   2702    // "If new value is null, abort this algorithm."
   2703    if (newValue === null) {
   2704        return;
   2705    }
   2706 
   2707    // "If node is an allowed child of "span":"
   2708    if (isAllowedChild(node, "span")) {
   2709        // "Reorder modifiable descendants of node's previousSibling."
   2710        reorderModifiableDescendants(node.previousSibling, command, newValue);
   2711 
   2712        // "Reorder modifiable descendants of node's nextSibling."
   2713        reorderModifiableDescendants(node.nextSibling, command, newValue);
   2714 
   2715        // "Wrap the one-node list consisting of node, with sibling criteria
   2716        // returning true for a simple modifiable element whose specified
   2717        // command value is equivalent to new value and whose effective command
   2718        // value is loosely equivalent to new value and false otherwise, and
   2719        // with new parent instructions returning null."
   2720        wrap([node],
   2721            function(node) {
   2722                return isSimpleModifiableElement(node)
   2723                    && areEquivalentValues(command, getSpecifiedCommandValue(node, command), newValue)
   2724                    && areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue);
   2725            },
   2726            function() { return null }
   2727        );
   2728    }
   2729 
   2730    // "If node is invisible, abort this algorithm."
   2731    if (isInvisible(node)) {
   2732        return;
   2733    }
   2734 
   2735    // "If the effective command value of command is loosely equivalent to new
   2736    // value on node, abort this algorithm."
   2737    if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
   2738        return;
   2739    }
   2740 
   2741    // "If node is not an allowed child of "span":"
   2742    if (!isAllowedChild(node, "span")) {
   2743        // "Let children be all children of node, omitting any that are
   2744        // Elements whose specified command value for command is neither null
   2745        // nor equivalent to new value."
   2746        var children = [];
   2747        for (var i = 0; i < node.childNodes.length; i++) {
   2748            if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) {
   2749                var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command);
   2750 
   2751                if (specifiedValue !== null
   2752                && !areEquivalentValues(command, newValue, specifiedValue)) {
   2753                    continue;
   2754                }
   2755            }
   2756            children.push(node.childNodes[i]);
   2757        }
   2758 
   2759        // "Force the value of each Node in children, with command and new
   2760        // value as in this invocation of the algorithm."
   2761        for (var i = 0; i < children.length; i++) {
   2762            forceValue(children[i], command, newValue);
   2763        }
   2764 
   2765        // "Abort this algorithm."
   2766        return;
   2767    }
   2768 
   2769    // "If the effective command value of command is loosely equivalent to new
   2770    // value on node, abort this algorithm."
   2771    if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
   2772        return;
   2773    }
   2774 
   2775    // "Let new parent be null."
   2776    var newParent = null;
   2777 
   2778    // "If the CSS styling flag is false:"
   2779    if (!cssStylingFlag) {
   2780        // "If command is "bold" and new value is "bold", let new parent be the
   2781        // result of calling createElement("b") on the ownerDocument of node."
   2782        if (command == "bold" && (newValue == "bold" || newValue == "700")) {
   2783            newParent = node.ownerDocument.createElement("b");
   2784        }
   2785 
   2786        // "If command is "italic" and new value is "italic", let new parent be
   2787        // the result of calling createElement("i") on the ownerDocument of
   2788        // node."
   2789        if (command == "italic" && newValue == "italic") {
   2790            newParent = node.ownerDocument.createElement("i");
   2791        }
   2792 
   2793        // "If command is "strikethrough" and new value is "line-through", let
   2794        // new parent be the result of calling createElement("s") on the
   2795        // ownerDocument of node."
   2796        if (command == "strikethrough" && newValue == "line-through") {
   2797            newParent = node.ownerDocument.createElement("s");
   2798        }
   2799 
   2800        // "If command is "underline" and new value is "underline", let new
   2801        // parent be the result of calling createElement("u") on the
   2802        // ownerDocument of node."
   2803        if (command == "underline" && newValue == "underline") {
   2804            newParent = node.ownerDocument.createElement("u");
   2805        }
   2806 
   2807        // "If command is "foreColor", and new value is fully opaque with red,
   2808        // green, and blue components in the range 0 to 255:"
   2809        if (command == "forecolor" && parseSimpleColor(newValue)) {
   2810            // "Let new parent be the result of calling createElement("font")
   2811            // on the ownerDocument of node."
   2812            newParent = node.ownerDocument.createElement("font");
   2813 
   2814            // "Set the color attribute of new parent to the result of applying
   2815            // the rules for serializing simple color values to new value
   2816            // (interpreted as a simple color)."
   2817            newParent.setAttribute("color", parseSimpleColor(newValue));
   2818        }
   2819 
   2820        // "If command is "fontName", let new parent be the result of calling
   2821        // createElement("font") on the ownerDocument of node, then set the
   2822        // face attribute of new parent to new value."
   2823        if (command == "fontname") {
   2824            newParent = node.ownerDocument.createElement("font");
   2825            newParent.face = newValue;
   2826        }
   2827    }
   2828 
   2829    // "If command is "createLink" or "unlink":"
   2830    if (command == "createlink" || command == "unlink") {
   2831        // "Let new parent be the result of calling createElement("a") on the
   2832        // ownerDocument of node."
   2833        newParent = node.ownerDocument.createElement("a");
   2834 
   2835        // "Set the href attribute of new parent to new value."
   2836        newParent.setAttribute("href", newValue);
   2837 
   2838        // "Let ancestor be node's parent."
   2839        var ancestor = node.parentNode;
   2840 
   2841        // "While ancestor is not null:"
   2842        while (ancestor) {
   2843            // "If ancestor is an a, set the tag name of ancestor to "span",
   2844            // and let ancestor be the result."
   2845            if (isHtmlElement(ancestor, "A")) {
   2846                ancestor = setTagName(ancestor, "span");
   2847            }
   2848 
   2849            // "Set ancestor to its parent."
   2850            ancestor = ancestor.parentNode;
   2851        }
   2852    }
   2853 
   2854    // "If command is "fontSize"; and new value is one of "x-small", "small",
   2855    // "medium", "large", "x-large", "xx-large", or "xxx-large"; and either the
   2856    // CSS styling flag is false, or new value is "xxx-large": let new parent
   2857    // be the result of calling createElement("font") on the ownerDocument of
   2858    // node, then set the size attribute of new parent to the number from the
   2859    // following table based on new value: [table omitted]"
   2860    if (command == "fontsize"
   2861    && ["x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"].indexOf(newValue) != -1
   2862    && (!cssStylingFlag || newValue == "xxx-large")) {
   2863        newParent = node.ownerDocument.createElement("font");
   2864        newParent.size = cssSizeToLegacy(newValue);
   2865    }
   2866 
   2867    // "If command is "subscript" or "superscript" and new value is
   2868    // "subscript", let new parent be the result of calling
   2869    // createElement("sub") on the ownerDocument of node."
   2870    if ((command == "subscript" || command == "superscript")
   2871    && newValue == "subscript") {
   2872        newParent = node.ownerDocument.createElement("sub");
   2873    }
   2874 
   2875    // "If command is "subscript" or "superscript" and new value is
   2876    // "superscript", let new parent be the result of calling
   2877    // createElement("sup") on the ownerDocument of node."
   2878    if ((command == "subscript" || command == "superscript")
   2879    && newValue == "superscript") {
   2880        newParent = node.ownerDocument.createElement("sup");
   2881    }
   2882 
   2883    // "If new parent is null, let new parent be the result of calling
   2884    // createElement("span") on the ownerDocument of node."
   2885    if (!newParent) {
   2886        newParent = node.ownerDocument.createElement("span");
   2887    }
   2888 
   2889    // "Insert new parent in node's parent before node."
   2890    node.parentNode.insertBefore(newParent, node);
   2891 
   2892    // "If the effective command value of command for new parent is not loosely
   2893    // equivalent to new value, and the relevant CSS property for command is
   2894    // not null, set that CSS property of new parent to new value (if the new
   2895    // value would be valid)."
   2896    var property = commands[command].relevantCssProperty;
   2897    if (property !== null
   2898    && !areLooselyEquivalentValues(command, getEffectiveCommandValue(newParent, command), newValue)) {
   2899        newParent.style[property] = newValue;
   2900    }
   2901 
   2902    // "If command is "strikethrough", and new value is "line-through", and the
   2903    // effective command value of "strikethrough" for new parent is not
   2904    // "line-through", set the "text-decoration" property of new parent to
   2905    // "line-through"."
   2906    if (command == "strikethrough"
   2907    && newValue == "line-through"
   2908    && getEffectiveCommandValue(newParent, "strikethrough") != "line-through") {
   2909        newParent.style.textDecoration = "line-through";
   2910    }
   2911 
   2912    // "If command is "underline", and new value is "underline", and the
   2913    // effective command value of "underline" for new parent is not
   2914    // "underline", set the "text-decoration" property of new parent to
   2915    // "underline"."
   2916    if (command == "underline"
   2917    && newValue == "underline"
   2918    && getEffectiveCommandValue(newParent, "underline") != "underline") {
   2919        newParent.style.textDecoration = "underline";
   2920    }
   2921 
   2922    // "Append node to new parent as its last child, preserving ranges."
   2923    movePreservingRanges(node, newParent, newParent.childNodes.length);
   2924 
   2925    // "If node is an Element and the effective command value of command for
   2926    // node is not loosely equivalent to new value:"
   2927    if (node.nodeType == Node.ELEMENT_NODE
   2928    && !areEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
   2929        // "Insert node into the parent of new parent before new parent,
   2930        // preserving ranges."
   2931        movePreservingRanges(node, newParent.parentNode, getNodeIndex(newParent));
   2932 
   2933        // "Remove new parent from its parent."
   2934        newParent.parentNode.removeChild(newParent);
   2935 
   2936        // "Let children be all children of node, omitting any that are
   2937        // Elements whose specified command value for command is neither null
   2938        // nor equivalent to new value."
   2939        var children = [];
   2940        for (var i = 0; i < node.childNodes.length; i++) {
   2941            if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) {
   2942                var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command);
   2943 
   2944                if (specifiedValue !== null
   2945                && !areEquivalentValues(command, newValue, specifiedValue)) {
   2946                    continue;
   2947                }
   2948            }
   2949            children.push(node.childNodes[i]);
   2950        }
   2951 
   2952        // "Force the value of each Node in children, with command and new
   2953        // value as in this invocation of the algorithm."
   2954        for (var i = 0; i < children.length; i++) {
   2955            forceValue(children[i], command, newValue);
   2956        }
   2957    }
   2958 }
   2959 
   2960 
   2961 //@}
   2962 ///// Setting the selection's value /////
   2963 //@{
   2964 
   2965 function setSelectionValue(command, newValue) {
   2966    // "If there is no formattable node effectively contained in the active
   2967    // range:"
   2968    if (!getAllEffectivelyContainedNodes(getActiveRange())
   2969    .some(isFormattableNode)) {
   2970        // "If command has inline command activated values, set the state
   2971        // override to true if new value is among them and false if it's not."
   2972        if ("inlineCommandActivatedValues" in commands[command]) {
   2973            setStateOverride(command, commands[command].inlineCommandActivatedValues
   2974                .indexOf(newValue) != -1);
   2975        }
   2976 
   2977        // "If command is "subscript", unset the state override for
   2978        // "superscript"."
   2979        if (command == "subscript") {
   2980            unsetStateOverride("superscript");
   2981        }
   2982 
   2983        // "If command is "superscript", unset the state override for
   2984        // "subscript"."
   2985        if (command == "superscript") {
   2986            unsetStateOverride("subscript");
   2987        }
   2988 
   2989        // "If new value is null, unset the value override (if any)."
   2990        if (newValue === null) {
   2991            unsetValueOverride(command);
   2992 
   2993        // "Otherwise, if command is "createLink" or it has a value specified,
   2994        // set the value override to new value."
   2995        } else if (command == "createlink" || "value" in commands[command]) {
   2996            setValueOverride(command, newValue);
   2997        }
   2998 
   2999        // "Abort these steps."
   3000        return;
   3001    }
   3002 
   3003    // "If the active range's start node is an editable Text node, and its
   3004    // start offset is neither zero nor its start node's length, call
   3005    // splitText() on the active range's start node, with argument equal to the
   3006    // active range's start offset. Then set the active range's start node to
   3007    // the result, and its start offset to zero."
   3008    if (isEditable(getActiveRange().startContainer)
   3009    && getActiveRange().startContainer.nodeType == Node.TEXT_NODE
   3010    && getActiveRange().startOffset != 0
   3011    && getActiveRange().startOffset != getNodeLength(getActiveRange().startContainer)) {
   3012        // Account for browsers not following range mutation rules
   3013        var newActiveRange = document.createRange();
   3014        var newNode;
   3015        if (getActiveRange().startContainer == getActiveRange().endContainer) {
   3016            var newEndOffset = getActiveRange().endOffset - getActiveRange().startOffset;
   3017            newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset);
   3018            newActiveRange.setEnd(newNode, newEndOffset);
   3019            getActiveRange().setEnd(newNode, newEndOffset);
   3020        } else {
   3021            newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset);
   3022        }
   3023        newActiveRange.setStart(newNode, 0);
   3024        getSelection().removeAllRanges();
   3025        getSelection().addRange(newActiveRange);
   3026 
   3027        getActiveRange().setStart(newNode, 0);
   3028    }
   3029 
   3030    // "If the active range's end node is an editable Text node, and its end
   3031    // offset is neither zero nor its end node's length, call splitText() on
   3032    // the active range's end node, with argument equal to the active range's
   3033    // end offset."
   3034    if (isEditable(getActiveRange().endContainer)
   3035    && getActiveRange().endContainer.nodeType == Node.TEXT_NODE
   3036    && getActiveRange().endOffset != 0
   3037    && getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer)) {
   3038        // IE seems to mutate the range incorrectly here, so we need correction
   3039        // here as well.  The active range will be temporarily in orphaned
   3040        // nodes, so calling getActiveRange() after splitText() but before
   3041        // fixing the range will throw an exception.
   3042        var activeRange = getActiveRange();
   3043        var newStart = [activeRange.startContainer, activeRange.startOffset];
   3044        var newEnd = [activeRange.endContainer, activeRange.endOffset];
   3045        activeRange.endContainer.splitText(activeRange.endOffset);
   3046        activeRange.setStart(newStart[0], newStart[1]);
   3047        activeRange.setEnd(newEnd[0], newEnd[1]);
   3048 
   3049        getSelection().removeAllRanges();
   3050        getSelection().addRange(activeRange);
   3051    }
   3052 
   3053    // "Let element list be all editable Elements effectively contained in the
   3054    // active range.
   3055    //
   3056    // "For each element in element list, clear the value of element."
   3057    getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
   3058        return isEditable(node) && node.nodeType == Node.ELEMENT_NODE;
   3059    }).forEach(function(element) {
   3060        clearValue(element, command);
   3061    });
   3062 
   3063    // "Let node list be all editable nodes effectively contained in the active
   3064    // range.
   3065    //
   3066    // "For each node in node list:"
   3067    getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(function(node) {
   3068        // "Push down values on node."
   3069        pushDownValues(node, command, newValue);
   3070 
   3071        // "If node is an allowed child of span, force the value of node."
   3072        if (isAllowedChild(node, "span")) {
   3073            forceValue(node, command, newValue);
   3074        }
   3075    });
   3076 }
   3077 
   3078 
   3079 //@}
   3080 ///// The backColor command /////
   3081 //@{
   3082 commands.backcolor = {
   3083    // Copy-pasted, same as hiliteColor
   3084    action: function(value) {
   3085        // Action is further copy-pasted, same as foreColor
   3086 
   3087        // "If value is not a valid CSS color, prepend "#" to it."
   3088        //
   3089        // "If value is still not a valid CSS color, or if it is currentColor,
   3090        // return false."
   3091        //
   3092        // Cheap hack for testing, no attempt to be comprehensive.
   3093        if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
   3094            value = "#" + value;
   3095        }
   3096        if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
   3097        && !parseSimpleColor(value)
   3098        && value.toLowerCase() != "transparent") {
   3099            return false;
   3100        }
   3101 
   3102        // "Set the selection's value to value."
   3103        setSelectionValue("backcolor", value);
   3104 
   3105        // "Return true."
   3106        return true;
   3107    }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor",
   3108    equivalentValues: function(val1, val2) {
   3109        // "Either both strings are valid CSS colors and have the same red,
   3110        // green, blue, and alpha components, or neither string is a valid CSS
   3111        // color."
   3112        return normalizeColor(val1) === normalizeColor(val2);
   3113    },
   3114 };
   3115 
   3116 //@}
   3117 ///// The bold command /////
   3118 //@{
   3119 commands.bold = {
   3120    action: function() {
   3121        // "If queryCommandState("bold") returns true, set the selection's
   3122        // value to "normal". Otherwise set the selection's value to "bold".
   3123        // Either way, return true."
   3124        if (myQueryCommandState("bold")) {
   3125            setSelectionValue("bold", "normal");
   3126        } else {
   3127            setSelectionValue("bold", "bold");
   3128        }
   3129        return true;
   3130    }, inlineCommandActivatedValues: ["bold", "600", "700", "800", "900"],
   3131    relevantCssProperty: "fontWeight",
   3132    equivalentValues: function(val1, val2) {
   3133        // "Either the two strings are equal, or one is "bold" and the other is
   3134        // "700", or one is "normal" and the other is "400"."
   3135        return val1 == val2
   3136            || (val1 == "bold" && val2 == "700")
   3137            || (val1 == "700" && val2 == "bold")
   3138            || (val1 == "normal" && val2 == "400")
   3139            || (val1 == "400" && val2 == "normal");
   3140    },
   3141 };
   3142 
   3143 //@}
   3144 ///// The createLink command /////
   3145 //@{
   3146 commands.createlink = {
   3147    action: function(value) {
   3148        // "If value is the empty string, return false."
   3149        if (value === "") {
   3150            return false;
   3151        }
   3152 
   3153        // "For each editable a element that has an href attribute and is an
   3154        // ancestor of some node effectively contained in the active range, set
   3155        // that a element's href attribute to value."
   3156        //
   3157        // TODO: We don't actually do this in tree order, not that it matters
   3158        // unless you're spying with mutation events.
   3159        getAllEffectivelyContainedNodes(getActiveRange()).forEach(function(node) {
   3160            getAncestors(node).forEach(function(ancestor) {
   3161                if (isEditable(ancestor)
   3162                && isHtmlElement(ancestor, "a")
   3163                && ancestor.hasAttribute("href")) {
   3164                    ancestor.setAttribute("href", value);
   3165                }
   3166            });
   3167        });
   3168 
   3169        // "Set the selection's value to value."
   3170        setSelectionValue("createlink", value);
   3171 
   3172        // "Return true."
   3173        return true;
   3174    }
   3175 };
   3176 
   3177 //@}
   3178 ///// The fontName command /////
   3179 //@{
   3180 commands.fontname = {
   3181    action: function(value) {
   3182        // "Set the selection's value to value, then return true."
   3183        setSelectionValue("fontname", value);
   3184        return true;
   3185    }, standardInlineValueCommand: true, relevantCssProperty: "fontFamily"
   3186 };
   3187 
   3188 //@}
   3189 ///// The fontSize command /////
   3190 //@{
   3191 
   3192 // Helper function for fontSize's action plus queryOutputHelper.  It's just the
   3193 // middle of fontSize's action, ripped out into its own function.  Returns null
   3194 // if the size is invalid.
   3195 function normalizeFontSize(value) {
   3196    // "Strip leading and trailing whitespace from value."
   3197    //
   3198    // Cheap hack, not following the actual algorithm.
   3199    value = value.trim();
   3200 
   3201    // "If value is not a valid floating point number, and would not be a valid
   3202    // floating point number if a single leading "+" character were stripped,
   3203    // return false."
   3204    if (!/^[-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?$/.test(value)) {
   3205        return null;
   3206    }
   3207 
   3208    var mode;
   3209 
   3210    // "If the first character of value is "+", delete the character and let
   3211    // mode be "relative-plus"."
   3212    if (value[0] == "+") {
   3213        value = value.slice(1);
   3214        mode = "relative-plus";
   3215    // "Otherwise, if the first character of value is "-", delete the character
   3216    // and let mode be "relative-minus"."
   3217    } else if (value[0] == "-") {
   3218        value = value.slice(1);
   3219        mode = "relative-minus";
   3220    // "Otherwise, let mode be "absolute"."
   3221    } else {
   3222        mode = "absolute";
   3223    }
   3224 
   3225    // "Apply the rules for parsing non-negative integers to value, and let
   3226    // number be the result."
   3227    //
   3228    // Another cheap hack.
   3229    var num = parseInt(value);
   3230 
   3231    // "If mode is "relative-plus", add three to number."
   3232    if (mode == "relative-plus") {
   3233        num += 3;
   3234    }
   3235 
   3236    // "If mode is "relative-minus", negate number, then add three to it."
   3237    if (mode == "relative-minus") {
   3238        num = 3 - num;
   3239    }
   3240 
   3241    // "If number is less than one, let number equal 1."
   3242    if (num < 1) {
   3243        num = 1;
   3244    }
   3245 
   3246    // "If number is greater than seven, let number equal 7."
   3247    if (num > 7) {
   3248        num = 7;
   3249    }
   3250 
   3251    // "Set value to the string here corresponding to number:" [table omitted]
   3252    value = {
   3253        1: "x-small",
   3254        2: "small",
   3255        3: "medium",
   3256        4: "large",
   3257        5: "x-large",
   3258        6: "xx-large",
   3259        7: "xxx-large"
   3260    }[num];
   3261 
   3262    return value;
   3263 }
   3264 
   3265 commands.fontsize = {
   3266    action: function(value) {
   3267        value = normalizeFontSize(value);
   3268        if (value === null) {
   3269            return false;
   3270        }
   3271 
   3272        // "Set the selection's value to value."
   3273        setSelectionValue("fontsize", value);
   3274 
   3275        // "Return true."
   3276        return true;
   3277    }, indeterm: function() {
   3278        // "True if among formattable nodes that are effectively contained in
   3279        // the active range, there are two that have distinct effective command
   3280        // values.  Otherwise false."
   3281        return getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)
   3282        .map(function(node) {
   3283            return getEffectiveCommandValue(node, "fontsize");
   3284        }).filter(function(value, i, arr) {
   3285            return arr.slice(0, i).indexOf(value) == -1;
   3286        }).length >= 2;
   3287    }, value: function() {
   3288        // "If the active range is null, return the empty string."
   3289        if (!getActiveRange()) {
   3290            return "";
   3291        }
   3292 
   3293        // "Let pixel size be the effective command value of the first
   3294        // formattable node that is effectively contained in the active range,
   3295        // or if there is no such node, the effective command value of the
   3296        // active range's start node, in either case interpreted as a number of
   3297        // pixels."
   3298        var node = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)[0];
   3299        if (node === undefined) {
   3300            node = getActiveRange().startContainer;
   3301        }
   3302        var pixelSize = getEffectiveCommandValue(node, "fontsize");
   3303 
   3304        // "Return the legacy font size for pixel size."
   3305        return getLegacyFontSize(pixelSize);
   3306    }, relevantCssProperty: "fontSize"
   3307 };
   3308 
   3309 function getLegacyFontSize(size) {
   3310    if (getLegacyFontSize.resultCache === undefined) {
   3311        getLegacyFontSize.resultCache = {};
   3312    }
   3313 
   3314    if (getLegacyFontSize.resultCache[size] !== undefined) {
   3315        return getLegacyFontSize.resultCache[size];
   3316    }
   3317 
   3318    // For convenience in other places in my code, I handle all sizes, not just
   3319    // pixel sizes as the spec says.  This means pixel sizes have to be passed
   3320    // in suffixed with "px", not as plain numbers.
   3321    if (normalizeFontSize(size) !== null) {
   3322        return getLegacyFontSize.resultCache[size] = cssSizeToLegacy(normalizeFontSize(size));
   3323    }
   3324 
   3325    if (["x-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"].indexOf(size) == -1
   3326    && !/^[0-9]+(\.[0-9]+)?(cm|mm|in|pt|pc|px)$/.test(size)) {
   3327        // There is no sensible legacy size for things like "2em".
   3328        return getLegacyFontSize.resultCache[size] = null;
   3329    }
   3330 
   3331    var font = document.createElement("font");
   3332    document.body.appendChild(font);
   3333    if (size == "xxx-large") {
   3334        font.size = 7;
   3335    } else {
   3336        font.style.fontSize = size;
   3337    }
   3338    var pixelSize = parseInt(getComputedStyle(font).fontSize);
   3339    document.body.removeChild(font);
   3340 
   3341    // "Let returned size be 1."
   3342    var returnedSize = 1;
   3343 
   3344    // "While returned size is less than 7:"
   3345    while (returnedSize < 7) {
   3346        // "Let lower bound be the resolved value of "font-size" in pixels
   3347        // of a font element whose size attribute is set to returned size."
   3348        var font = document.createElement("font");
   3349        font.size = returnedSize;
   3350        document.body.appendChild(font);
   3351        var lowerBound = parseInt(getComputedStyle(font).fontSize);
   3352 
   3353        // "Let upper bound be the resolved value of "font-size" in pixels
   3354        // of a font element whose size attribute is set to one plus
   3355        // returned size."
   3356        font.size = 1 + returnedSize;
   3357        var upperBound = parseInt(getComputedStyle(font).fontSize);
   3358        document.body.removeChild(font);
   3359 
   3360        // "Let average be the average of upper bound and lower bound."
   3361        var average = (upperBound + lowerBound)/2;
   3362 
   3363        // "If pixel size is less than average, return the one-element
   3364        // string consisting of the digit returned size."
   3365        if (pixelSize < average) {
   3366            return getLegacyFontSize.resultCache[size] = String(returnedSize);
   3367        }
   3368 
   3369        // "Add one to returned size."
   3370        returnedSize++;
   3371    }
   3372 
   3373    // "Return "7"."
   3374    return getLegacyFontSize.resultCache[size] = "7";
   3375 }
   3376 
   3377 //@}
   3378 ///// The foreColor command /////
   3379 //@{
   3380 commands.forecolor = {
   3381    action: function(value) {
   3382        // Copy-pasted, same as backColor and hiliteColor
   3383 
   3384        // "If value is not a valid CSS color, prepend "#" to it."
   3385        //
   3386        // "If value is still not a valid CSS color, or if it is currentColor,
   3387        // return false."
   3388        //
   3389        // Cheap hack for testing, no attempt to be comprehensive.
   3390        if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
   3391            value = "#" + value;
   3392        }
   3393        if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
   3394        && !parseSimpleColor(value)
   3395        && value.toLowerCase() != "transparent") {
   3396            return false;
   3397        }
   3398 
   3399        // "Set the selection's value to value."
   3400        setSelectionValue("forecolor", value);
   3401 
   3402        // "Return true."
   3403        return true;
   3404    }, standardInlineValueCommand: true, relevantCssProperty: "color",
   3405    equivalentValues: function(val1, val2) {
   3406        // "Either both strings are valid CSS colors and have the same red,
   3407        // green, blue, and alpha components, or neither string is a valid CSS
   3408        // color."
   3409        return normalizeColor(val1) === normalizeColor(val2);
   3410    },
   3411 };
   3412 
   3413 //@}
   3414 ///// The hiliteColor command /////
   3415 //@{
   3416 commands.hilitecolor = {
   3417    // Copy-pasted, same as backColor
   3418    action: function(value) {
   3419        // Action is further copy-pasted, same as foreColor
   3420 
   3421        // "If value is not a valid CSS color, prepend "#" to it."
   3422        //
   3423        // "If value is still not a valid CSS color, or if it is currentColor,
   3424        // return false."
   3425        //
   3426        // Cheap hack for testing, no attempt to be comprehensive.
   3427        if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
   3428            value = "#" + value;
   3429        }
   3430        if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
   3431        && !parseSimpleColor(value)
   3432        && value.toLowerCase() != "transparent") {
   3433            return false;
   3434        }
   3435 
   3436        // "Set the selection's value to value."
   3437        setSelectionValue("hilitecolor", value);
   3438 
   3439        // "Return true."
   3440        return true;
   3441    }, indeterm: function() {
   3442        // "True if among editable Text nodes that are effectively contained in
   3443        // the active range, there are two that have distinct effective command
   3444        // values.  Otherwise false."
   3445        return getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
   3446            return isEditable(node) && node.nodeType == Node.TEXT_NODE;
   3447        }).map(function(node) {
   3448            return getEffectiveCommandValue(node, "hilitecolor");
   3449        }).filter(function(value, i, arr) {
   3450            return arr.slice(0, i).indexOf(value) == -1;
   3451        }).length >= 2;
   3452    }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor",
   3453    equivalentValues: function(val1, val2) {
   3454        // "Either both strings are valid CSS colors and have the same red,
   3455        // green, blue, and alpha components, or neither string is a valid CSS
   3456        // color."
   3457        return normalizeColor(val1) === normalizeColor(val2);
   3458    },
   3459 };
   3460 
   3461 //@}
   3462 ///// The italic command /////
   3463 //@{
   3464 commands.italic = {
   3465    action: function() {
   3466        // "If queryCommandState("italic") returns true, set the selection's
   3467        // value to "normal". Otherwise set the selection's value to "italic".
   3468        // Either way, return true."
   3469        if (myQueryCommandState("italic")) {
   3470            setSelectionValue("italic", "normal");
   3471        } else {
   3472            setSelectionValue("italic", "italic");
   3473        }
   3474        return true;
   3475    }, inlineCommandActivatedValues: ["italic", "oblique"],
   3476    relevantCssProperty: "fontStyle"
   3477 };
   3478 
   3479 //@}
   3480 ///// The removeFormat command /////
   3481 //@{
   3482 commands.removeformat = {
   3483    action: function() {
   3484        // "A removeFormat candidate is an editable HTML element with local
   3485        // name "abbr", "acronym", "b", "bdi", "bdo", "big", "blink", "cite",
   3486        // "code", "dfn", "em", "font", "i", "ins", "kbd", "mark", "nobr", "q",
   3487        // "s", "samp", "small", "span", "strike", "strong", "sub", "sup",
   3488        // "tt", "u", or "var"."
   3489        function isRemoveFormatCandidate(node) {
   3490            return isEditable(node)
   3491                && isHtmlElement(node, ["abbr", "acronym", "b", "bdi", "bdo",
   3492                "big", "blink", "cite", "code", "dfn", "em", "font", "i",
   3493                "ins", "kbd", "mark", "nobr", "q", "s", "samp", "small",
   3494                "span", "strike", "strong", "sub", "sup", "tt", "u", "var"]);
   3495        }
   3496 
   3497        // "Let elements to remove be a list of every removeFormat candidate
   3498        // effectively contained in the active range."
   3499        var elementsToRemove = getAllEffectivelyContainedNodes(getActiveRange(), isRemoveFormatCandidate);
   3500 
   3501        // "For each element in elements to remove:"
   3502        elementsToRemove.forEach(function(element) {
   3503            // "While element has children, insert the first child of element
   3504            // into the parent of element immediately before element,
   3505            // preserving ranges."
   3506            while (element.hasChildNodes()) {
   3507                movePreservingRanges(element.firstChild, element.parentNode, getNodeIndex(element));
   3508            }
   3509 
   3510            // "Remove element from its parent."
   3511            element.parentNode.removeChild(element);
   3512        });
   3513 
   3514        // "If the active range's start node is an editable Text node, and its
   3515        // start offset is neither zero nor its start node's length, call
   3516        // splitText() on the active range's start node, with argument equal to
   3517        // the active range's start offset. Then set the active range's start
   3518        // node to the result, and its start offset to zero."
   3519        if (isEditable(getActiveRange().startContainer)
   3520        && getActiveRange().startContainer.nodeType == Node.TEXT_NODE
   3521        && getActiveRange().startOffset != 0
   3522        && getActiveRange().startOffset != getNodeLength(getActiveRange().startContainer)) {
   3523            // Account for browsers not following range mutation rules
   3524            if (getActiveRange().startContainer == getActiveRange().endContainer) {
   3525                var newEnd = getActiveRange().endOffset - getActiveRange().startOffset;
   3526                var newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset);
   3527                getActiveRange().setStart(newNode, 0);
   3528                getActiveRange().setEnd(newNode, newEnd);
   3529            } else {
   3530                getActiveRange().setStart(getActiveRange().startContainer.splitText(getActiveRange().startOffset), 0);
   3531            }
   3532        }
   3533 
   3534        // "If the active range's end node is an editable Text node, and its
   3535        // end offset is neither zero nor its end node's length, call
   3536        // splitText() on the active range's end node, with argument equal to
   3537        // the active range's end offset."
   3538        if (isEditable(getActiveRange().endContainer)
   3539        && getActiveRange().endContainer.nodeType == Node.TEXT_NODE
   3540        && getActiveRange().endOffset != 0
   3541        && getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer)) {
   3542            // IE seems to mutate the range incorrectly here, so we need
   3543            // correction here as well.  Have to be careful to set the range to
   3544            // something not including the text node so that getActiveRange()
   3545            // doesn't throw an exception due to a temporarily detached
   3546            // endpoint.
   3547            var newStart = [getActiveRange().startContainer, getActiveRange().startOffset];
   3548            var newEnd = [getActiveRange().endContainer, getActiveRange().endOffset];
   3549            getActiveRange().setEnd(document.documentElement, 0);
   3550            newEnd[0].splitText(newEnd[1]);
   3551            getActiveRange().setStart(newStart[0], newStart[1]);
   3552            getActiveRange().setEnd(newEnd[0], newEnd[1]);
   3553        }
   3554 
   3555        // "Let node list consist of all editable nodes effectively contained
   3556        // in the active range."
   3557        //
   3558        // "For each node in node list, while node's parent is a removeFormat
   3559        // candidate in the same editing host as node, split the parent of the
   3560        // one-node list consisting of node."
   3561        getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(function(node) {
   3562            while (isRemoveFormatCandidate(node.parentNode)
   3563            && inSameEditingHost(node.parentNode, node)) {
   3564                splitParent([node]);
   3565            }
   3566        });
   3567 
   3568        // "For each of the entries in the following list, in the given order,
   3569        // set the selection's value to null, with command as given."
   3570        [
   3571            "subscript",
   3572            "bold",
   3573            "fontname",
   3574            "fontsize",
   3575            "forecolor",
   3576            "hilitecolor",
   3577            "italic",
   3578            "strikethrough",
   3579            "underline",
   3580        ].forEach(function(command) {
   3581            setSelectionValue(command, null);
   3582        });
   3583 
   3584        // "Return true."
   3585        return true;
   3586    }
   3587 };
   3588 
   3589 //@}
   3590 ///// The strikethrough command /////
   3591 //@{
   3592 commands.strikethrough = {
   3593    action: function() {
   3594        // "If queryCommandState("strikethrough") returns true, set the
   3595        // selection's value to null. Otherwise set the selection's value to
   3596        // "line-through".  Either way, return true."
   3597        if (myQueryCommandState("strikethrough")) {
   3598            setSelectionValue("strikethrough", null);
   3599        } else {
   3600            setSelectionValue("strikethrough", "line-through");
   3601        }
   3602        return true;
   3603    }, inlineCommandActivatedValues: ["line-through"]
   3604 };
   3605 
   3606 //@}
   3607 ///// The subscript command /////
   3608 //@{
   3609 commands.subscript = {
   3610    action: function() {
   3611        // "Call queryCommandState("subscript"), and let state be the result."
   3612        var state = myQueryCommandState("subscript");
   3613 
   3614        // "Set the selection's value to null."
   3615        setSelectionValue("subscript", null);
   3616 
   3617        // "If state is false, set the selection's value to "subscript"."
   3618        if (!state) {
   3619            setSelectionValue("subscript", "subscript");
   3620        }
   3621 
   3622        // "Return true."
   3623        return true;
   3624    }, indeterm: function() {
   3625        // "True if either among formattable nodes that are effectively
   3626        // contained in the active range, there is at least one with effective
   3627        // command value "subscript" and at least one with some other effective
   3628        // command value; or if there is some formattable node effectively
   3629        // contained in the active range with effective command value "mixed".
   3630        // Otherwise false."
   3631        var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode);
   3632        return (nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") == "subscript" })
   3633            && nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") != "subscript" }))
   3634            || nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") == "mixed" });
   3635    }, inlineCommandActivatedValues: ["subscript"],
   3636 };
   3637 
   3638 //@}
   3639 ///// The superscript command /////
   3640 //@{
   3641 commands.superscript = {
   3642    action: function() {
   3643        // "Call queryCommandState("superscript"), and let state be the
   3644        // result."
   3645        var state = myQueryCommandState("superscript");
   3646 
   3647        // "Set the selection's value to null."
   3648        setSelectionValue("superscript", null);
   3649 
   3650        // "If state is false, set the selection's value to "superscript"."
   3651        if (!state) {
   3652            setSelectionValue("superscript", "superscript");
   3653        }
   3654 
   3655        // "Return true."
   3656        return true;
   3657    }, indeterm: function() {
   3658        // "True if either among formattable nodes that are effectively
   3659        // contained in the active range, there is at least one with effective
   3660        // command value "superscript" and at least one with some other
   3661        // effective command value; or if there is some formattable node
   3662        // effectively contained in the active range with effective command
   3663        // value "mixed".  Otherwise false."
   3664        var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode);
   3665        return (nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") == "superscript" })
   3666            && nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") != "superscript" }))
   3667            || nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") == "mixed" });
   3668    }, inlineCommandActivatedValues: ["superscript"],
   3669 };
   3670 
   3671 //@}
   3672 ///// The underline command /////
   3673 //@{
   3674 commands.underline = {
   3675    action: function() {
   3676        // "If queryCommandState("underline") returns true, set the selection's
   3677        // value to null. Otherwise set the selection's value to "underline".
   3678        // Either way, return true."
   3679        if (myQueryCommandState("underline")) {
   3680            setSelectionValue("underline", null);
   3681        } else {
   3682            setSelectionValue("underline", "underline");
   3683        }
   3684        return true;
   3685    }, inlineCommandActivatedValues: ["underline"]
   3686 };
   3687 
   3688 //@}
   3689 ///// The unlink command /////
   3690 //@{
   3691 commands.unlink = {
   3692    action: function() {
   3693        // "Let hyperlinks be a list of every a element that has an href
   3694        // attribute and is contained in the active range or is an ancestor of
   3695        // one of its boundary points."
   3696        //
   3697        // As usual, take care to ensure it's tree order.  The correctness of
   3698        // the following is left as an exercise for the reader.
   3699        var range = getActiveRange();
   3700        var hyperlinks = [];
   3701        for (
   3702            var node = range.startContainer;
   3703            node;
   3704            node = node.parentNode
   3705        ) {
   3706            if (isHtmlElement(node, "A")
   3707            && node.hasAttribute("href")) {
   3708                hyperlinks.unshift(node);
   3709            }
   3710        }
   3711        for (
   3712            var node = range.startContainer;
   3713            node != nextNodeDescendants(range.endContainer);
   3714            node = nextNode(node)
   3715        ) {
   3716            if (isHtmlElement(node, "A")
   3717            && node.hasAttribute("href")
   3718            && (isContained(node, range)
   3719            || isAncestor(node, range.endContainer)
   3720            || node == range.endContainer)) {
   3721                hyperlinks.push(node);
   3722            }
   3723        }
   3724 
   3725        // "Clear the value of each member of hyperlinks."
   3726        for (var i = 0; i < hyperlinks.length; i++) {
   3727            clearValue(hyperlinks[i], "unlink");
   3728        }
   3729 
   3730        // "Return true."
   3731        return true;
   3732    }
   3733 };
   3734 
   3735 //@}
   3736 
   3737 /////////////////////////////////////
   3738 ///// Block formatting commands /////
   3739 /////////////////////////////////////
   3740 
   3741 ///// Block formatting command definitions /////
   3742 //@{
   3743 
   3744 // "An indentation element is either a blockquote, or a div that has a style
   3745 // attribute that sets "margin" or some subproperty of it."
   3746 function isIndentationElement(node) {
   3747    if (!isHtmlElement(node)) {
   3748        return false;
   3749    }
   3750 
   3751    if (node.tagName == "BLOCKQUOTE") {
   3752        return true;
   3753    }
   3754 
   3755    if (node.tagName != "DIV") {
   3756        return false;
   3757    }
   3758 
   3759    for (var i = 0; i < node.style.length; i++) {
   3760        // Approximate check
   3761        if (/^(-[a-z]+-)?margin/.test(node.style[i])) {
   3762            return true;
   3763        }
   3764    }
   3765 
   3766    return false;
   3767 }
   3768 
   3769 // "A simple indentation element is an indentation element that has no
   3770 // attributes except possibly
   3771 //
   3772 //   * "a style attribute that sets no properties other than "margin",
   3773 //     "border", "padding", or subproperties of those; and/or
   3774 //   * "a dir attribute."
   3775 function isSimpleIndentationElement(node) {
   3776    if (!isIndentationElement(node)) {
   3777        return false;
   3778    }
   3779 
   3780    for (var i = 0; i < node.attributes.length; i++) {
   3781        if (!isHtmlNamespace(node.attributes[i].namespaceURI)
   3782        || ["style", "dir"].indexOf(node.attributes[i].name) == -1) {
   3783            return false;
   3784        }
   3785    }
   3786 
   3787    for (var i = 0; i < node.style.length; i++) {
   3788        // This is approximate, but it works well enough for my purposes.
   3789        if (!/^(-[a-z]+-)?(margin|border|padding)/.test(node.style[i])) {
   3790            return false;
   3791        }
   3792    }
   3793 
   3794    return true;
   3795 }
   3796 
   3797 // "A non-list single-line container is an HTML element with local name
   3798 // "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "listing", "p", "pre",
   3799 // or "xmp"."
   3800 function isNonListSingleLineContainer(node) {
   3801    return isHtmlElement(node, ["address", "div", "h1", "h2", "h3", "h4", "h5",
   3802        "h6", "listing", "p", "pre", "xmp"]);
   3803 }
   3804 
   3805 // "A single-line container is either a non-list single-line container, or an
   3806 // HTML element with local name "li", "dt", or "dd"."
   3807 function isSingleLineContainer(node) {
   3808    return isNonListSingleLineContainer(node)
   3809        || isHtmlElement(node, ["li", "dt", "dd"]);
   3810 }
   3811 
   3812 function getBlockNodeOf(node) {
   3813    // "While node is an inline node, set node to its parent."
   3814    while (isInlineNode(node)) {
   3815        node = node.parentNode;
   3816    }
   3817 
   3818    // "Return node."
   3819    return node;
   3820 }
   3821 
   3822 //@}
   3823 ///// Assorted block formatting command algorithms /////
   3824 //@{
   3825 
   3826 function fixDisallowedAncestors(node) {
   3827    // "If node is not editable, abort these steps."
   3828    if (!isEditable(node)) {
   3829        return;
   3830    }
   3831 
   3832    // "If node is not an allowed child of any of its ancestors in the same
   3833    // editing host:"
   3834    if (getAncestors(node).every(function(ancestor) {
   3835        return !inSameEditingHost(node, ancestor)
   3836            || !isAllowedChild(node, ancestor)
   3837    })) {
   3838        // "If node is a dd or dt, wrap the one-node list consisting of node,
   3839        // with sibling criteria returning true for any dl with no attributes
   3840        // and false otherwise, and new parent instructions returning the
   3841        // result of calling createElement("dl") on the context object. Then
   3842        // abort these steps."
   3843        if (isHtmlElement(node, ["dd", "dt"])) {
   3844            wrap([node],
   3845                function(sibling) { return isHtmlElement(sibling, "dl") && !sibling.attributes.length },
   3846                function() { return document.createElement("dl") });
   3847            return;
   3848        }
   3849 
   3850        // "If "p" is not an allowed child of the editing host of node, abort
   3851        // these steps."
   3852        if (!isAllowedChild("p", getEditingHostOf(node))) {
   3853            return;
   3854        }
   3855 
   3856        // "If node is not a prohibited paragraph child, abort these steps."
   3857        if (!isProhibitedParagraphChild(node)) {
   3858            return;
   3859        }
   3860 
   3861        // "Set the tag name of node to the default single-line container name,
   3862        // and let node be the result."
   3863        node = setTagName(node, defaultSingleLineContainerName);
   3864 
   3865        // "Fix disallowed ancestors of node."
   3866        fixDisallowedAncestors(node);
   3867 
   3868        // "Let children be node's children."
   3869        var children = [].slice.call(node.childNodes);
   3870 
   3871        // "For each child in children, if child is a prohibited paragraph
   3872        // child:"
   3873        children.filter(isProhibitedParagraphChild)
   3874        .forEach(function(child) {
   3875            // "Record the values of the one-node list consisting of child, and
   3876            // let values be the result."
   3877            var values = recordValues([child]);
   3878 
   3879            // "Split the parent of the one-node list consisting of child."
   3880            splitParent([child]);
   3881 
   3882            // "Restore the values from values."
   3883            restoreValues(values);
   3884        });
   3885 
   3886        // "Abort these steps."
   3887        return;
   3888    }
   3889 
   3890    // "Record the values of the one-node list consisting of node, and let
   3891    // values be the result."
   3892    var values = recordValues([node]);
   3893 
   3894    // "While node is not an allowed child of its parent, split the parent of
   3895    // the one-node list consisting of node."
   3896    while (!isAllowedChild(node, node.parentNode)) {
   3897        splitParent([node]);
   3898    }
   3899 
   3900    // "Restore the values from values."
   3901    restoreValues(values);
   3902 }
   3903 
   3904 function normalizeSublists(item) {
   3905    // "If item is not an li or it is not editable or its parent is not
   3906    // editable, abort these steps."
   3907    if (!isHtmlElement(item, "LI")
   3908    || !isEditable(item)
   3909    || !isEditable(item.parentNode)) {
   3910        return;
   3911    }
   3912 
   3913    // "Let new item be null."
   3914    var newItem = null;
   3915 
   3916    // "While item has an ol or ul child:"
   3917    while ([].some.call(item.childNodes, function (node) { return isHtmlElement(node, ["OL", "UL"]) })) {
   3918        // "Let child be the last child of item."
   3919        var child = item.lastChild;
   3920 
   3921        // "If child is an ol or ul, or new item is null and child is a Text
   3922        // node whose data consists of zero of more space characters:"
   3923        if (isHtmlElement(child, ["OL", "UL"])
   3924        || (!newItem && child.nodeType == Node.TEXT_NODE && /^[ \t\n\f\r]*$/.test(child.data))) {
   3925            // "Set new item to null."
   3926            newItem = null;
   3927 
   3928            // "Insert child into the parent of item immediately following
   3929            // item, preserving ranges."
   3930            movePreservingRanges(child, item.parentNode, 1 + getNodeIndex(item));
   3931 
   3932        // "Otherwise:"
   3933        } else {
   3934            // "If new item is null, let new item be the result of calling
   3935            // createElement("li") on the ownerDocument of item, then insert
   3936            // new item into the parent of item immediately after item."
   3937            if (!newItem) {
   3938                newItem = item.ownerDocument.createElement("li");
   3939                item.parentNode.insertBefore(newItem, item.nextSibling);
   3940            }
   3941 
   3942            // "Insert child into new item as its first child, preserving
   3943            // ranges."
   3944            movePreservingRanges(child, newItem, 0);
   3945        }
   3946    }
   3947 }
   3948 
   3949 function getSelectionListState() {
   3950    // "If the active range is null, return "none"."
   3951    if (!getActiveRange()) {
   3952        return "none";
   3953    }
   3954 
   3955    // "Block-extend the active range, and let new range be the result."
   3956    var newRange = blockExtend(getActiveRange());
   3957 
   3958    // "Let node list be a list of nodes, initially empty."
   3959    //
   3960    // "For each node contained in new range, append node to node list if the
   3961    // last member of node list (if any) is not an ancestor of node; node is
   3962    // editable; node is not an indentation element; and node is either an ol
   3963    // or ul, or the child of an ol or ul, or an allowed child of "li"."
   3964    var nodeList = getContainedNodes(newRange, function(node) {
   3965        return isEditable(node)
   3966            && !isIndentationElement(node)
   3967            && (isHtmlElement(node, ["ol", "ul"])
   3968            || isHtmlElement(node.parentNode, ["ol", "ul"])
   3969            || isAllowedChild(node, "li"));
   3970    });
   3971 
   3972    // "If node list is empty, return "none"."
   3973    if (!nodeList.length) {
   3974        return "none";
   3975    }
   3976 
   3977    // "If every member of node list is either an ol or the child of an ol or
   3978    // the child of an li child of an ol, and none is a ul or an ancestor of a
   3979    // ul, return "ol"."
   3980    if (nodeList.every(function(node) {
   3981        return isHtmlElement(node, "ol")
   3982            || isHtmlElement(node.parentNode, "ol")
   3983            || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ol"));
   3984    })
   3985    && !nodeList.some(function(node) { return isHtmlElement(node, "ul") || ("querySelector" in node && node.querySelector("ul")) })) {
   3986        return "ol";
   3987    }
   3988 
   3989    // "If every member of node list is either a ul or the child of a ul or the
   3990    // child of an li child of a ul, and none is an ol or an ancestor of an ol,
   3991    // return "ul"."
   3992    if (nodeList.every(function(node) {
   3993        return isHtmlElement(node, "ul")
   3994            || isHtmlElement(node.parentNode, "ul")
   3995            || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ul"));
   3996    })
   3997    && !nodeList.some(function(node) { return isHtmlElement(node, "ol") || ("querySelector" in node && node.querySelector("ol")) })) {
   3998        return "ul";
   3999    }
   4000 
   4001    var hasOl = nodeList.some(function(node) {
   4002        return isHtmlElement(node, "ol")
   4003            || isHtmlElement(node.parentNode, "ol")
   4004            || ("querySelector" in node && node.querySelector("ol"))
   4005            || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ol"));
   4006    });
   4007    var hasUl = nodeList.some(function(node) {
   4008        return isHtmlElement(node, "ul")
   4009            || isHtmlElement(node.parentNode, "ul")
   4010            || ("querySelector" in node && node.querySelector("ul"))
   4011            || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ul"));
   4012    });
   4013    // "If some member of node list is either an ol or the child or ancestor of
   4014    // an ol or the child of an li child of an ol, and some member of node list
   4015    // is either a ul or the child or ancestor of a ul or the child of an li
   4016    // child of a ul, return "mixed"."
   4017    if (hasOl && hasUl) {
   4018        return "mixed";
   4019    }
   4020 
   4021    // "If some member of node list is either an ol or the child or ancestor of
   4022    // an ol or the child of an li child of an ol, return "mixed ol"."
   4023    if (hasOl) {
   4024        return "mixed ol";
   4025    }
   4026 
   4027    // "If some member of node list is either a ul or the child or ancestor of
   4028    // a ul or the child of an li child of a ul, return "mixed ul"."
   4029    if (hasUl) {
   4030        return "mixed ul";
   4031    }
   4032 
   4033    // "Return "none"."
   4034    return "none";
   4035 }
   4036 
   4037 function getAlignmentValue(node) {
   4038    // "While node is neither null nor an Element, or it is an Element but its
   4039    // "display" property has resolved value "inline" or "none", set node to
   4040    // its parent."
   4041    while ((node && node.nodeType != Node.ELEMENT_NODE)
   4042    || (node.nodeType == Node.ELEMENT_NODE
   4043    && ["inline", "none"].indexOf(getComputedStyle(node).display) != -1)) {
   4044        node = node.parentNode;
   4045    }
   4046 
   4047    // "If node is not an Element, return "left"."
   4048    if (!node || node.nodeType != Node.ELEMENT_NODE) {
   4049        return "left";
   4050    }
   4051 
   4052    var resolvedValue = getComputedStyle(node).textAlign
   4053        // Hack around browser non-standardness
   4054        .replace(/^-(moz|webkit)-/, "")
   4055        .replace(/^auto$/, "start");
   4056 
   4057    // "If node's "text-align" property has resolved value "start", return
   4058    // "left" if the directionality of node is "ltr", "right" if it is "rtl"."
   4059    if (resolvedValue == "start") {
   4060        return getDirectionality(node) == "ltr" ? "left" : "right";
   4061    }
   4062 
   4063    // "If node's "text-align" property has resolved value "end", return
   4064    // "right" if the directionality of node is "ltr", "left" if it is "rtl"."
   4065    if (resolvedValue == "end") {
   4066        return getDirectionality(node) == "ltr" ? "right" : "left";
   4067    }
   4068 
   4069    // "If node's "text-align" property has resolved value "center", "justify",
   4070    // "left", or "right", return that value."
   4071    if (["center", "justify", "left", "right"].indexOf(resolvedValue) != -1) {
   4072        return resolvedValue;
   4073    }
   4074 
   4075    // "Return "left"."
   4076    return "left";
   4077 }
   4078 
   4079 function getNextEquivalentPoint(node, offset) {
   4080    // "If node's length is zero, return null."
   4081    if (getNodeLength(node) == 0) {
   4082        return null;
   4083    }
   4084 
   4085    // "If offset is node's length, and node's parent is not null, and node is
   4086    // an inline node, return (node's parent, 1 + node's index)."
   4087    if (offset == getNodeLength(node)
   4088    && node.parentNode
   4089    && isInlineNode(node)) {
   4090        return [node.parentNode, 1 + getNodeIndex(node)];
   4091    }
   4092 
   4093    // "If node has a child with index offset, and that child's length is not
   4094    // zero, and that child is an inline node, return (that child, 0)."
   4095    if (0 <= offset
   4096    && offset < node.childNodes.length
   4097    && getNodeLength(node.childNodes[offset]) != 0
   4098    && isInlineNode(node.childNodes[offset])) {
   4099        return [node.childNodes[offset], 0];
   4100    }
   4101 
   4102    // "Return null."
   4103    return null;
   4104 }
   4105 
   4106 function getPreviousEquivalentPoint(node, offset) {
   4107    // "If node's length is zero, return null."
   4108    if (getNodeLength(node) == 0) {
   4109        return null;
   4110    }
   4111 
   4112    // "If offset is 0, and node's parent is not null, and node is an inline
   4113    // node, return (node's parent, node's index)."
   4114    if (offset == 0
   4115    && node.parentNode
   4116    && isInlineNode(node)) {
   4117        return [node.parentNode, getNodeIndex(node)];
   4118    }
   4119 
   4120    // "If node has a child with index offset − 1, and that child's length is
   4121    // not zero, and that child is an inline node, return (that child, that
   4122    // child's length)."
   4123    if (0 <= offset - 1
   4124    && offset - 1 < node.childNodes.length
   4125    && getNodeLength(node.childNodes[offset - 1]) != 0
   4126    && isInlineNode(node.childNodes[offset - 1])) {
   4127        return [node.childNodes[offset - 1], getNodeLength(node.childNodes[offset - 1])];
   4128    }
   4129 
   4130    // "Return null."
   4131    return null;
   4132 }
   4133 
   4134 function getFirstEquivalentPoint(node, offset) {
   4135    // "While (node, offset)'s previous equivalent point is not null, set
   4136    // (node, offset) to its previous equivalent point."
   4137    var prev;
   4138    while (prev = getPreviousEquivalentPoint(node, offset)) {
   4139        node = prev[0];
   4140        offset = prev[1];
   4141    }
   4142 
   4143    // "Return (node, offset)."
   4144    return [node, offset];
   4145 }
   4146 
   4147 function getLastEquivalentPoint(node, offset) {
   4148    // "While (node, offset)'s next equivalent point is not null, set (node,
   4149    // offset) to its next equivalent point."
   4150    var next;
   4151    while (next = getNextEquivalentPoint(node, offset)) {
   4152        node = next[0];
   4153        offset = next[1];
   4154    }
   4155 
   4156    // "Return (node, offset)."
   4157    return [node, offset];
   4158 }
   4159 
   4160 //@}
   4161 ///// Block-extending a range /////
   4162 //@{
   4163 
   4164 // "A boundary point (node, offset) is a block start point if either node's
   4165 // parent is null and offset is zero; or node has a child with index offset −
   4166 // 1, and that child is either a visible block node or a visible br."
   4167 function isBlockStartPoint(node, offset) {
   4168    return (!node.parentNode && offset == 0)
   4169        || (0 <= offset - 1
   4170        && offset - 1 < node.childNodes.length
   4171        && isVisible(node.childNodes[offset - 1])
   4172        && (isBlockNode(node.childNodes[offset - 1])
   4173        || isHtmlElement(node.childNodes[offset - 1], "br")));
   4174 }
   4175 
   4176 // "A boundary point (node, offset) is a block end point if either node's
   4177 // parent is null and offset is node's length; or node has a child with index
   4178 // offset, and that child is a visible block node."
   4179 function isBlockEndPoint(node, offset) {
   4180    return (!node.parentNode && offset == getNodeLength(node))
   4181        || (offset < node.childNodes.length
   4182        && isVisible(node.childNodes[offset])
   4183        && isBlockNode(node.childNodes[offset]));
   4184 }
   4185 
   4186 // "A boundary point is a block boundary point if it is either a block start
   4187 // point or a block end point."
   4188 function isBlockBoundaryPoint(node, offset) {
   4189    return isBlockStartPoint(node, offset)
   4190        || isBlockEndPoint(node, offset);
   4191 }
   4192 
   4193 function blockExtend(range) {
   4194    // "Let start node, start offset, end node, and end offset be the start
   4195    // and end nodes and offsets of the range."
   4196    var startNode = range.startContainer;
   4197    var startOffset = range.startOffset;
   4198    var endNode = range.endContainer;
   4199    var endOffset = range.endOffset;
   4200 
   4201    // "If some ancestor container of start node is an li, set start offset to
   4202    // the index of the last such li in tree order, and set start node to that
   4203    // li's parent."
   4204    var liAncestors = getAncestors(startNode).concat(startNode)
   4205        .filter(function(ancestor) { return isHtmlElement(ancestor, "li") })
   4206        .slice(-1);
   4207    if (liAncestors.length) {
   4208        startOffset = getNodeIndex(liAncestors[0]);
   4209        startNode = liAncestors[0].parentNode;
   4210    }
   4211 
   4212    // "If (start node, start offset) is not a block start point, repeat the
   4213    // following steps:"
   4214    if (!isBlockStartPoint(startNode, startOffset)) do {
   4215        // "If start offset is zero, set it to start node's index, then set
   4216        // start node to its parent."
   4217        if (startOffset == 0) {
   4218            startOffset = getNodeIndex(startNode);
   4219            startNode = startNode.parentNode;
   4220 
   4221        // "Otherwise, subtract one from start offset."
   4222        } else {
   4223            startOffset--;
   4224        }
   4225 
   4226        // "If (start node, start offset) is a block boundary point, break from
   4227        // this loop."
   4228    } while (!isBlockBoundaryPoint(startNode, startOffset));
   4229 
   4230    // "While start offset is zero and start node's parent is not null, set
   4231    // start offset to start node's index, then set start node to its parent."
   4232    while (startOffset == 0
   4233    && startNode.parentNode) {
   4234        startOffset = getNodeIndex(startNode);
   4235        startNode = startNode.parentNode;
   4236    }
   4237 
   4238    // "If some ancestor container of end node is an li, set end offset to one
   4239    // plus the index of the last such li in tree order, and set end node to
   4240    // that li's parent."
   4241    var liAncestors = getAncestors(endNode).concat(endNode)
   4242        .filter(function(ancestor) { return isHtmlElement(ancestor, "li") })
   4243        .slice(-1);
   4244    if (liAncestors.length) {
   4245        endOffset = 1 + getNodeIndex(liAncestors[0]);
   4246        endNode = liAncestors[0].parentNode;
   4247    }
   4248 
   4249    // "If (end node, end offset) is not a block end point, repeat the
   4250    // following steps:"
   4251    if (!isBlockEndPoint(endNode, endOffset)) do {
   4252        // "If end offset is end node's length, set it to one plus end node's
   4253        // index, then set end node to its parent."
   4254        if (endOffset == getNodeLength(endNode)) {
   4255            endOffset = 1 + getNodeIndex(endNode);
   4256            endNode = endNode.parentNode;
   4257 
   4258        // "Otherwise, add one to end offset.
   4259        } else {
   4260            endOffset++;
   4261        }
   4262 
   4263        // "If (end node, end offset) is a block boundary point, break from
   4264        // this loop."
   4265    } while (!isBlockBoundaryPoint(endNode, endOffset));
   4266 
   4267    // "While end offset is end node's length and end node's parent is not
   4268    // null, set end offset to one plus end node's index, then set end node to
   4269    // its parent."
   4270    while (endOffset == getNodeLength(endNode)
   4271    && endNode.parentNode) {
   4272        endOffset = 1 + getNodeIndex(endNode);
   4273        endNode = endNode.parentNode;
   4274    }
   4275 
   4276    // "Let new range be a new range whose start and end nodes and offsets
   4277    // are start node, start offset, end node, and end offset."
   4278    var newRange = startNode.ownerDocument.createRange();
   4279    newRange.setStart(startNode, startOffset);
   4280    newRange.setEnd(endNode, endOffset);
   4281 
   4282    // "Return new range."
   4283    return newRange;
   4284 }
   4285 
   4286 function followsLineBreak(node) {
   4287    // "Let offset be zero."
   4288    var offset = 0;
   4289 
   4290    // "While (node, offset) is not a block boundary point:"
   4291    while (!isBlockBoundaryPoint(node, offset)) {
   4292        // "If node has a visible child with index offset minus one, return
   4293        // false."
   4294        if (0 <= offset - 1
   4295        && offset - 1 < node.childNodes.length
   4296        && isVisible(node.childNodes[offset - 1])) {
   4297            return false;
   4298        }
   4299 
   4300        // "If offset is zero or node has no children, set offset to node's
   4301        // index, then set node to its parent."
   4302        if (offset == 0
   4303        || !node.hasChildNodes()) {
   4304            offset = getNodeIndex(node);
   4305            node = node.parentNode;
   4306 
   4307        // "Otherwise, set node to its child with index offset minus one, then
   4308        // set offset to node's length."
   4309        } else {
   4310            node = node.childNodes[offset - 1];
   4311            offset = getNodeLength(node);
   4312        }
   4313    }
   4314 
   4315    // "Return true."
   4316    return true;
   4317 }
   4318 
   4319 function precedesLineBreak(node) {
   4320    // "Let offset be node's length."
   4321    var offset = getNodeLength(node);
   4322 
   4323    // "While (node, offset) is not a block boundary point:"
   4324    while (!isBlockBoundaryPoint(node, offset)) {
   4325        // "If node has a visible child with index offset, return false."
   4326        if (offset < node.childNodes.length
   4327        && isVisible(node.childNodes[offset])) {
   4328            return false;
   4329        }
   4330 
   4331        // "If offset is node's length or node has no children, set offset to
   4332        // one plus node's index, then set node to its parent."
   4333        if (offset == getNodeLength(node)
   4334        || !node.hasChildNodes()) {
   4335            offset = 1 + getNodeIndex(node);
   4336            node = node.parentNode;
   4337 
   4338        // "Otherwise, set node to its child with index offset and set offset
   4339        // to zero."
   4340        } else {
   4341            node = node.childNodes[offset];
   4342            offset = 0;
   4343        }
   4344    }
   4345 
   4346    // "Return true."
   4347    return true;
   4348 }
   4349 
   4350 //@}
   4351 ///// Recording and restoring overrides /////
   4352 //@{
   4353 
   4354 function recordCurrentOverrides() {
   4355    // "Let overrides be a list of (string, string or boolean) ordered pairs,
   4356    // initially empty."
   4357    var overrides = [];
   4358 
   4359    // "If there is a value override for "createLink", add ("createLink", value
   4360    // override for "createLink") to overrides."
   4361    if (getValueOverride("createlink") !== undefined) {
   4362        overrides.push(["createlink", getValueOverride("createlink")]);
   4363    }
   4364 
   4365    // "For each command in the list "bold", "italic", "strikethrough",
   4366    // "subscript", "superscript", "underline", in order: if there is a state
   4367    // override for command, add (command, command's state override) to
   4368    // overrides."
   4369    ["bold", "italic", "strikethrough", "subscript", "superscript",
   4370    "underline"].forEach(function(command) {
   4371        if (getStateOverride(command) !== undefined) {
   4372            overrides.push([command, getStateOverride(command)]);
   4373        }
   4374    });
   4375 
   4376    // "For each command in the list "fontName", "fontSize", "foreColor",
   4377    // "hiliteColor", in order: if there is a value override for command, add
   4378    // (command, command's value override) to overrides."
   4379    ["fontname", "fontsize", "forecolor",
   4380    "hilitecolor"].forEach(function(command) {
   4381        if (getValueOverride(command) !== undefined) {
   4382            overrides.push([command, getValueOverride(command)]);
   4383        }
   4384    });
   4385 
   4386    // "Return overrides."
   4387    return overrides;
   4388 }
   4389 
   4390 function recordCurrentStatesAndValues() {
   4391    // "Let overrides be a list of (string, string or boolean) ordered pairs,
   4392    // initially empty."
   4393    var overrides = [];
   4394 
   4395    // "Let node be the first formattable node effectively contained in the
   4396    // active range, or null if there is none."
   4397    var node = getAllEffectivelyContainedNodes(getActiveRange())
   4398        .filter(isFormattableNode)[0];
   4399 
   4400    // "If node is null, return overrides."
   4401    if (!node) {
   4402        return overrides;
   4403    }
   4404 
   4405    // "Add ("createLink", node's effective command value for "createLink") to
   4406    // overrides."
   4407    overrides.push(["createlink", getEffectiveCommandValue(node, "createlink")]);
   4408 
   4409    // "For each command in the list "bold", "italic", "strikethrough",
   4410    // "subscript", "superscript", "underline", in order: if node's effective
   4411    // command value for command is one of its inline command activated values,
   4412    // add (command, true) to overrides, and otherwise add (command, false) to
   4413    // overrides."
   4414    ["bold", "italic", "strikethrough", "subscript", "superscript",
   4415    "underline"].forEach(function(command) {
   4416        if (commands[command].inlineCommandActivatedValues
   4417        .indexOf(getEffectiveCommandValue(node, command)) != -1) {
   4418            overrides.push([command, true]);
   4419        } else {
   4420            overrides.push([command, false]);
   4421        }
   4422    });
   4423 
   4424    // "For each command in the list "fontName", "foreColor", "hiliteColor", in
   4425    // order: add (command, command's value) to overrides."
   4426    ["fontname", "fontsize", "forecolor", "hilitecolor"].forEach(function(command) {
   4427        overrides.push([command, commands[command].value()]);
   4428    });
   4429 
   4430    // "Add ("fontSize", node's effective command value for "fontSize") to
   4431    // overrides."
   4432    overrides.push(["fontsize", getEffectiveCommandValue(node, "fontsize")]);
   4433 
   4434    // "Return overrides."
   4435    return overrides;
   4436 }
   4437 
   4438 function restoreStatesAndValues(overrides) {
   4439    // "Let node be the first formattable node effectively contained in the
   4440    // active range, or null if there is none."
   4441    var node = getAllEffectivelyContainedNodes(getActiveRange())
   4442        .filter(isFormattableNode)[0];
   4443 
   4444    // "If node is not null, then for each (command, override) pair in
   4445    // overrides, in order:"
   4446    if (node) {
   4447        for (var i = 0; i < overrides.length; i++) {
   4448            var command = overrides[i][0];
   4449            var override = overrides[i][1];
   4450 
   4451            // "If override is a boolean, and queryCommandState(command)
   4452            // returns something different from override, take the action for
   4453            // command, with value equal to the empty string."
   4454            if (typeof override == "boolean"
   4455            && myQueryCommandState(command) != override) {
   4456                commands[command].action("");
   4457 
   4458            // "Otherwise, if override is a string, and command is neither
   4459            // "createLink" nor "fontSize", and queryCommandValue(command)
   4460            // returns something not equivalent to override, take the action
   4461            // for command, with value equal to override."
   4462            } else if (typeof override == "string"
   4463            && command != "createlink"
   4464            && command != "fontsize"
   4465            && !areEquivalentValues(command, myQueryCommandValue(command), override)) {
   4466                commands[command].action(override);
   4467 
   4468            // "Otherwise, if override is a string; and command is
   4469            // "createLink"; and either there is a value override for
   4470            // "createLink" that is not equal to override, or there is no value
   4471            // override for "createLink" and node's effective command value for
   4472            // "createLink" is not equal to override: take the action for
   4473            // "createLink", with value equal to override."
   4474            } else if (typeof override == "string"
   4475            && command == "createlink"
   4476            && (
   4477                (
   4478                    getValueOverride("createlink") !== undefined
   4479                    && getValueOverride("createlink") !== override
   4480                ) || (
   4481                    getValueOverride("createlink") === undefined
   4482                    && getEffectiveCommandValue(node, "createlink") !== override
   4483                )
   4484            )) {
   4485                commands.createlink.action(override);
   4486 
   4487            // "Otherwise, if override is a string; and command is "fontSize";
   4488            // and either there is a value override for "fontSize" that is not
   4489            // equal to override, or there is no value override for "fontSize"
   4490            // and node's effective command value for "fontSize" is not loosely
   4491            // equivalent to override:"
   4492            } else if (typeof override == "string"
   4493            && command == "fontsize"
   4494            && (
   4495                (
   4496                    getValueOverride("fontsize") !== undefined
   4497                    && getValueOverride("fontsize") !== override
   4498                ) || (
   4499                    getValueOverride("fontsize") === undefined
   4500                    && !areLooselyEquivalentValues(command, getEffectiveCommandValue(node, "fontsize"), override)
   4501                )
   4502            )) {
   4503                // "Convert override to an integer number of pixels, and set
   4504                // override to the legacy font size for the result."
   4505                override = getLegacyFontSize(override);
   4506 
   4507                // "Take the action for "fontSize", with value equal to
   4508                // override."
   4509                commands.fontsize.action(override);
   4510 
   4511            // "Otherwise, continue this loop from the beginning."
   4512            } else {
   4513                continue;
   4514            }
   4515 
   4516            // "Set node to the first formattable node effectively contained in
   4517            // the active range, if there is one."
   4518            node = getAllEffectivelyContainedNodes(getActiveRange())
   4519                .filter(isFormattableNode)[0]
   4520                || node;
   4521        }
   4522 
   4523    // "Otherwise, for each (command, override) pair in overrides, in order:"
   4524    } else {
   4525        for (var i = 0; i < overrides.length; i++) {
   4526            var command = overrides[i][0];
   4527            var override = overrides[i][1];
   4528 
   4529            // "If override is a boolean, set the state override for command to
   4530            // override."
   4531            if (typeof override == "boolean") {
   4532                setStateOverride(command, override);
   4533            }
   4534 
   4535            // "If override is a string, set the value override for command to
   4536            // override."
   4537            if (typeof override == "string") {
   4538                setValueOverride(command, override);
   4539            }
   4540        }
   4541    }
   4542 }
   4543 
   4544 //@}
   4545 ///// Deleting the selection /////
   4546 //@{
   4547 
   4548 // The flags argument is a dictionary that can have blockMerging,
   4549 // stripWrappers, and/or direction as keys.
   4550 function deleteSelection(flags) {
   4551    if (flags === undefined) {
   4552        flags = {};
   4553    }
   4554 
   4555    var blockMerging = "blockMerging" in flags ? Boolean(flags.blockMerging) : true;
   4556    var stripWrappers = "stripWrappers" in flags ? Boolean(flags.stripWrappers) : true;
   4557    var direction = "direction" in flags ? flags.direction : "forward";
   4558 
   4559    // "If the active range is null, abort these steps and do nothing."
   4560    if (!getActiveRange()) {
   4561        return;
   4562    }
   4563 
   4564    // "Canonicalize whitespace at the active range's start."
   4565    canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset);
   4566 
   4567    // "Canonicalize whitespace at the active range's end."
   4568    canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset);
   4569 
   4570    // "Let (start node, start offset) be the last equivalent point for the
   4571    // active range's start."
   4572    var start = getLastEquivalentPoint(getActiveRange().startContainer, getActiveRange().startOffset);
   4573    var startNode = start[0];
   4574    var startOffset = start[1];
   4575 
   4576    // "Let (end node, end offset) be the first equivalent point for the active
   4577    // range's end."
   4578    var end = getFirstEquivalentPoint(getActiveRange().endContainer, getActiveRange().endOffset);
   4579    var endNode = end[0];
   4580    var endOffset = end[1];
   4581 
   4582    // "If (end node, end offset) is not after (start node, start offset):"
   4583    if (getPosition(endNode, endOffset, startNode, startOffset) !== "after") {
   4584        // "If direction is "forward", call collapseToStart() on the context
   4585        // object's Selection."
   4586        //
   4587        // Here and in a few other places, we check rangeCount to work around a
   4588        // WebKit bug: it will sometimes incorrectly remove ranges from the
   4589        // selection if nodes are removed, so collapseToStart() will throw.
   4590        // This will break everything if we're using an actual selection, but
   4591        // if getActiveRange() is really just returning globalRange and that's
   4592        // all we care about, it will work fine.  I only add the extra check
   4593        // for errors I actually hit in testing.
   4594        if (direction == "forward") {
   4595            if (getSelection().rangeCount) {
   4596                getSelection().collapseToStart();
   4597            }
   4598            getActiveRange().collapse(true);
   4599 
   4600        // "Otherwise, call collapseToEnd() on the context object's Selection."
   4601        } else {
   4602            getSelection().collapseToEnd();
   4603            getActiveRange().collapse(false);
   4604        }
   4605 
   4606        // "Abort these steps."
   4607        return;
   4608    }
   4609 
   4610    // "If start node is a Text node and start offset is 0, set start offset to
   4611    // the index of start node, then set start node to its parent."
   4612    if (startNode.nodeType == Node.TEXT_NODE
   4613    && startOffset == 0) {
   4614        startOffset = getNodeIndex(startNode);
   4615        startNode = startNode.parentNode;
   4616    }
   4617 
   4618    // "If end node is a Text node and end offset is its length, set end offset
   4619    // to one plus the index of end node, then set end node to its parent."
   4620    if (endNode.nodeType == Node.TEXT_NODE
   4621    && endOffset == getNodeLength(endNode)) {
   4622        endOffset = 1 + getNodeIndex(endNode);
   4623        endNode = endNode.parentNode;
   4624    }
   4625 
   4626    // "Call collapse(start node, start offset) on the context object's
   4627    // Selection."
   4628    getSelection().collapse(startNode, startOffset);
   4629    getActiveRange().setStart(startNode, startOffset);
   4630 
   4631    // "Call extend(end node, end offset) on the context object's Selection."
   4632    getSelection().extend(endNode, endOffset);
   4633    getActiveRange().setEnd(endNode, endOffset);
   4634 
   4635    // "Let start block be the active range's start node."
   4636    var startBlock = getActiveRange().startContainer;
   4637 
   4638    // "While start block's parent is in the same editing host and start block
   4639    // is an inline node, set start block to its parent."
   4640    while (inSameEditingHost(startBlock, startBlock.parentNode)
   4641    && isInlineNode(startBlock)) {
   4642        startBlock = startBlock.parentNode;
   4643    }
   4644 
   4645    // "If start block is neither a block node nor an editing host, or "span"
   4646    // is not an allowed child of start block, or start block is a td or th,
   4647    // set start block to null."
   4648    if ((!isBlockNode(startBlock) && !isEditingHost(startBlock))
   4649    || !isAllowedChild("span", startBlock)
   4650    || isHtmlElement(startBlock, ["td", "th"])) {
   4651        startBlock = null;
   4652    }
   4653 
   4654    // "Let end block be the active range's end node."
   4655    var endBlock = getActiveRange().endContainer;
   4656 
   4657    // "While end block's parent is in the same editing host and end block is
   4658    // an inline node, set end block to its parent."
   4659    while (inSameEditingHost(endBlock, endBlock.parentNode)
   4660    && isInlineNode(endBlock)) {
   4661        endBlock = endBlock.parentNode;
   4662    }
   4663 
   4664    // "If end block is neither a block node nor an editing host, or "span" is
   4665    // not an allowed child of end block, or end block is a td or th, set end
   4666    // block to null."
   4667    if ((!isBlockNode(endBlock) && !isEditingHost(endBlock))
   4668    || !isAllowedChild("span", endBlock)
   4669    || isHtmlElement(endBlock, ["td", "th"])) {
   4670        endBlock = null;
   4671    }
   4672 
   4673    // "Record current states and values, and let overrides be the result."
   4674    var overrides = recordCurrentStatesAndValues();
   4675 
   4676    // "If start node and end node are the same, and start node is an editable
   4677    // Text node:"
   4678    if (startNode == endNode
   4679    && isEditable(startNode)
   4680    && startNode.nodeType == Node.TEXT_NODE) {
   4681        // "Call deleteData(start offset, end offset − start offset) on start
   4682        // node."
   4683        startNode.deleteData(startOffset, endOffset - startOffset);
   4684 
   4685        // "Canonicalize whitespace at (start node, start offset), with fix
   4686        // collapsed space false."
   4687        canonicalizeWhitespace(startNode, startOffset, false);
   4688 
   4689        // "If direction is "forward", call collapseToStart() on the context
   4690        // object's Selection."
   4691        if (direction == "forward") {
   4692            if (getSelection().rangeCount) {
   4693                getSelection().collapseToStart();
   4694            }
   4695            getActiveRange().collapse(true);
   4696 
   4697        // "Otherwise, call collapseToEnd() on the context object's Selection."
   4698        } else {
   4699            getSelection().collapseToEnd();
   4700            getActiveRange().collapse(false);
   4701        }
   4702 
   4703        // "Restore states and values from overrides."
   4704        restoreStatesAndValues(overrides);
   4705 
   4706        // "Abort these steps."
   4707        return;
   4708    }
   4709 
   4710    // "If start node is an editable Text node, call deleteData() on it, with
   4711    // start offset as the first argument and (length of start node − start
   4712    // offset) as the second argument."
   4713    if (isEditable(startNode)
   4714    && startNode.nodeType == Node.TEXT_NODE) {
   4715        startNode.deleteData(startOffset, getNodeLength(startNode) - startOffset);
   4716    }
   4717 
   4718    // "Let node list be a list of nodes, initially empty."
   4719    //
   4720    // "For each node contained in the active range, append node to node list
   4721    // if the last member of node list (if any) is not an ancestor of node;
   4722    // node is editable; and node is not a thead, tbody, tfoot, tr, th, or td."
   4723    var nodeList = getContainedNodes(getActiveRange(),
   4724        function(node) {
   4725            return isEditable(node)
   4726                && !isHtmlElement(node, ["thead", "tbody", "tfoot", "tr", "th", "td"]);
   4727        }
   4728    );
   4729 
   4730    // "For each node in node list:"
   4731    for (var i = 0; i < nodeList.length; i++) {
   4732        var node = nodeList[i];
   4733 
   4734        // "Let parent be the parent of node."
   4735        var parent_ = node.parentNode;
   4736 
   4737        // "Remove node from parent."
   4738        parent_.removeChild(node);
   4739 
   4740        // "If the block node of parent has no visible children, and parent is
   4741        // editable or an editing host, call createElement("br") on the context
   4742        // object and append the result as the last child of parent."
   4743        if (![].some.call(getBlockNodeOf(parent_).childNodes, isVisible)
   4744        && (isEditable(parent_) || isEditingHost(parent_))) {
   4745            parent_.appendChild(document.createElement("br"));
   4746        }
   4747 
   4748        // "If strip wrappers is true or parent is not an ancestor container of
   4749        // start node, while parent is an editable inline node with length 0,
   4750        // let grandparent be the parent of parent, then remove parent from
   4751        // grandparent, then set parent to grandparent."
   4752        if (stripWrappers
   4753        || (!isAncestor(parent_, startNode) && parent_ != startNode)) {
   4754            while (isEditable(parent_)
   4755            && isInlineNode(parent_)
   4756            && getNodeLength(parent_) == 0) {
   4757                var grandparent = parent_.parentNode;
   4758                grandparent.removeChild(parent_);
   4759                parent_ = grandparent;
   4760            }
   4761        }
   4762    }
   4763 
   4764    // "If end node is an editable Text node, call deleteData(0, end offset) on
   4765    // it."
   4766    if (isEditable(endNode)
   4767    && endNode.nodeType == Node.TEXT_NODE) {
   4768        endNode.deleteData(0, endOffset);
   4769    }
   4770 
   4771    // "Canonicalize whitespace at the active range's start, with fix collapsed
   4772    // space false."
   4773    canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset, false);
   4774 
   4775    // "Canonicalize whitespace at the active range's end, with fix collapsed
   4776    // space false."
   4777    canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset, false);
   4778 
   4779    // "If block merging is false, or start block or end block is null, or
   4780    // start block is not in the same editing host as end block, or start block
   4781    // and end block are the same:"
   4782    if (!blockMerging
   4783    || !startBlock
   4784    || !endBlock
   4785    || !inSameEditingHost(startBlock, endBlock)
   4786    || startBlock == endBlock) {
   4787        // "If direction is "forward", call collapseToStart() on the context
   4788        // object's Selection."
   4789        if (direction == "forward") {
   4790            if (getSelection().rangeCount) {
   4791                getSelection().collapseToStart();
   4792            }
   4793            getActiveRange().collapse(true);
   4794 
   4795        // "Otherwise, call collapseToEnd() on the context object's Selection."
   4796        } else {
   4797            if (getSelection().rangeCount) {
   4798                getSelection().collapseToEnd();
   4799            }
   4800            getActiveRange().collapse(false);
   4801        }
   4802 
   4803        // "Restore states and values from overrides."
   4804        restoreStatesAndValues(overrides);
   4805 
   4806        // "Abort these steps."
   4807        return;
   4808    }
   4809 
   4810    // "If start block has one child, which is a collapsed block prop, remove
   4811    // its child from it."
   4812    if (startBlock.children.length == 1
   4813    && isCollapsedBlockProp(startBlock.firstChild)) {
   4814        startBlock.removeChild(startBlock.firstChild);
   4815    }
   4816 
   4817    // "If start block is an ancestor of end block:"
   4818    if (isAncestor(startBlock, endBlock)) {
   4819        // "Let reference node be end block."
   4820        var referenceNode = endBlock;
   4821 
   4822        // "While reference node is not a child of start block, set reference
   4823        // node to its parent."
   4824        while (referenceNode.parentNode != startBlock) {
   4825            referenceNode = referenceNode.parentNode;
   4826        }
   4827 
   4828        // "Call collapse() on the context object's Selection, with first
   4829        // argument start block and second argument the index of reference
   4830        // node."
   4831        getSelection().collapse(startBlock, getNodeIndex(referenceNode));
   4832        getActiveRange().setStart(startBlock, getNodeIndex(referenceNode));
   4833        getActiveRange().collapse(true);
   4834 
   4835        // "If end block has no children:"
   4836        if (!endBlock.hasChildNodes()) {
   4837            // "While end block is editable and is the only child of its parent
   4838            // and is not a child of start block, let parent equal end block,
   4839            // then remove end block from parent, then set end block to
   4840            // parent."
   4841            while (isEditable(endBlock)
   4842            && endBlock.parentNode.childNodes.length == 1
   4843            && endBlock.parentNode != startBlock) {
   4844                var parent_ = endBlock;
   4845                parent_.removeChild(endBlock);
   4846                endBlock = parent_;
   4847            }
   4848 
   4849            // "If end block is editable and is not an inline node, and its
   4850            // previousSibling and nextSibling are both inline nodes, call
   4851            // createElement("br") on the context object and insert it into end
   4852            // block's parent immediately after end block."
   4853            if (isEditable(endBlock)
   4854            && !isInlineNode(endBlock)
   4855            && isInlineNode(endBlock.previousSibling)
   4856            && isInlineNode(endBlock.nextSibling)) {
   4857                endBlock.parentNode.insertBefore(document.createElement("br"), endBlock.nextSibling);
   4858            }
   4859 
   4860            // "If end block is editable, remove it from its parent."
   4861            if (isEditable(endBlock)) {
   4862                endBlock.parentNode.removeChild(endBlock);
   4863            }
   4864 
   4865            // "Restore states and values from overrides."
   4866            restoreStatesAndValues(overrides);
   4867 
   4868            // "Abort these steps."
   4869            return;
   4870        }
   4871 
   4872        // "If end block's firstChild is not an inline node, restore states and
   4873        // values from overrides, then abort these steps."
   4874        if (!isInlineNode(endBlock.firstChild)) {
   4875            restoreStatesAndValues(overrides);
   4876            return;
   4877        }
   4878 
   4879        // "Let children be a list of nodes, initially empty."
   4880        var children = [];
   4881 
   4882        // "Append the first child of end block to children."
   4883        children.push(endBlock.firstChild);
   4884 
   4885        // "While children's last member is not a br, and children's last
   4886        // member's nextSibling is an inline node, append children's last
   4887        // member's nextSibling to children."
   4888        while (!isHtmlElement(children[children.length - 1], "br")
   4889        && isInlineNode(children[children.length - 1].nextSibling)) {
   4890            children.push(children[children.length - 1].nextSibling);
   4891        }
   4892 
   4893        // "Record the values of children, and let values be the result."
   4894        var values = recordValues(children);
   4895 
   4896        // "While children's first member's parent is not start block, split
   4897        // the parent of children."
   4898        while (children[0].parentNode != startBlock) {
   4899            splitParent(children);
   4900        }
   4901 
   4902        // "If children's first member's previousSibling is an editable br,
   4903        // remove that br from its parent."
   4904        if (isEditable(children[0].previousSibling)
   4905        && isHtmlElement(children[0].previousSibling, "br")) {
   4906            children[0].parentNode.removeChild(children[0].previousSibling);
   4907        }
   4908 
   4909    // "Otherwise, if start block is a descendant of end block:"
   4910    } else if (isDescendant(startBlock, endBlock)) {
   4911        // "Call collapse() on the context object's Selection, with first
   4912        // argument start block and second argument start block's length."
   4913        getSelection().collapse(startBlock, getNodeLength(startBlock));
   4914        getActiveRange().setStart(startBlock, getNodeLength(startBlock));
   4915        getActiveRange().collapse(true);
   4916 
   4917        // "Let reference node be start block."
   4918        var referenceNode = startBlock;
   4919 
   4920        // "While reference node is not a child of end block, set reference
   4921        // node to its parent."
   4922        while (referenceNode.parentNode != endBlock) {
   4923            referenceNode = referenceNode.parentNode;
   4924        }
   4925 
   4926        // "If reference node's nextSibling is an inline node and start block's
   4927        // lastChild is a br, remove start block's lastChild from it."
   4928        if (isInlineNode(referenceNode.nextSibling)
   4929        && isHtmlElement(startBlock.lastChild, "br")) {
   4930            startBlock.removeChild(startBlock.lastChild);
   4931        }
   4932 
   4933        // "Let nodes to move be a list of nodes, initially empty."
   4934        var nodesToMove = [];
   4935 
   4936        // "If reference node's nextSibling is neither null nor a block node,
   4937        // append it to nodes to move."
   4938        if (referenceNode.nextSibling
   4939        && !isBlockNode(referenceNode.nextSibling)) {
   4940            nodesToMove.push(referenceNode.nextSibling);
   4941        }
   4942 
   4943        // "While nodes to move is nonempty and its last member isn't a br and
   4944        // its last member's nextSibling is neither null nor a block node,
   4945        // append its last member's nextSibling to nodes to move."
   4946        if (nodesToMove.length
   4947        && !isHtmlElement(nodesToMove[nodesToMove.length - 1], "br")
   4948        && nodesToMove[nodesToMove.length - 1].nextSibling
   4949        && !isBlockNode(nodesToMove[nodesToMove.length - 1].nextSibling)) {
   4950            nodesToMove.push(nodesToMove[nodesToMove.length - 1].nextSibling);
   4951        }
   4952 
   4953        // "Record the values of nodes to move, and let values be the result."
   4954        var values = recordValues(nodesToMove);
   4955 
   4956        // "For each node in nodes to move, append node as the last child of
   4957        // start block, preserving ranges."
   4958        nodesToMove.forEach(function(node) {
   4959            movePreservingRanges(node, startBlock, -1);
   4960        });
   4961 
   4962    // "Otherwise:"
   4963    } else {
   4964        // "Call collapse() on the context object's Selection, with first
   4965        // argument start block and second argument start block's length."
   4966        getSelection().collapse(startBlock, getNodeLength(startBlock));
   4967        getActiveRange().setStart(startBlock, getNodeLength(startBlock));
   4968        getActiveRange().collapse(true);
   4969 
   4970        // "If end block's firstChild is an inline node and start block's
   4971        // lastChild is a br, remove start block's lastChild from it."
   4972        if (isInlineNode(endBlock.firstChild)
   4973        && isHtmlElement(startBlock.lastChild, "br")) {
   4974            startBlock.removeChild(startBlock.lastChild);
   4975        }
   4976 
   4977        // "Record the values of end block's children, and let values be the
   4978        // result."
   4979        var values = recordValues([].slice.call(endBlock.childNodes));
   4980 
   4981        // "While end block has children, append the first child of end block
   4982        // to start block, preserving ranges."
   4983        while (endBlock.hasChildNodes()) {
   4984            movePreservingRanges(endBlock.firstChild, startBlock, -1);
   4985        }
   4986 
   4987        // "While end block has no children, let parent be the parent of end
   4988        // block, then remove end block from parent, then set end block to
   4989        // parent."
   4990        while (!endBlock.hasChildNodes()) {
   4991            var parent_ = endBlock.parentNode;
   4992            parent_.removeChild(endBlock);
   4993            endBlock = parent_;
   4994        }
   4995    }
   4996 
   4997    // "Let ancestor be start block."
   4998    var ancestor = startBlock;
   4999 
   5000    // "While ancestor has an inclusive ancestor ol in the same editing host
   5001    // whose nextSibling is also an ol in the same editing host, or an
   5002    // inclusive ancestor ul in the same editing host whose nextSibling is also
   5003    // a ul in the same editing host:"
   5004    while (getInclusiveAncestors(ancestor).some(function(node) {
   5005        return inSameEditingHost(ancestor, node)
   5006            && (
   5007                (isHtmlElement(node, "ol") && isHtmlElement(node.nextSibling, "ol"))
   5008                || (isHtmlElement(node, "ul") && isHtmlElement(node.nextSibling, "ul"))
   5009            ) && inSameEditingHost(ancestor, node.nextSibling);
   5010    })) {
   5011        // "While ancestor and its nextSibling are not both ols in the same
   5012        // editing host, and are also not both uls in the same editing host,
   5013        // set ancestor to its parent."
   5014        while (!(
   5015            isHtmlElement(ancestor, "ol")
   5016            && isHtmlElement(ancestor.nextSibling, "ol")
   5017            && inSameEditingHost(ancestor, ancestor.nextSibling)
   5018        ) && !(
   5019            isHtmlElement(ancestor, "ul")
   5020            && isHtmlElement(ancestor.nextSibling, "ul")
   5021            && inSameEditingHost(ancestor, ancestor.nextSibling)
   5022        )) {
   5023            ancestor = ancestor.parentNode;
   5024        }
   5025 
   5026        // "While ancestor's nextSibling has children, append ancestor's
   5027        // nextSibling's firstChild as the last child of ancestor, preserving
   5028        // ranges."
   5029        while (ancestor.nextSibling.hasChildNodes()) {
   5030            movePreservingRanges(ancestor.nextSibling.firstChild, ancestor, -1);
   5031        }
   5032 
   5033        // "Remove ancestor's nextSibling from its parent."
   5034        ancestor.parentNode.removeChild(ancestor.nextSibling);
   5035    }
   5036 
   5037    // "Restore the values from values."
   5038    restoreValues(values);
   5039 
   5040    // "If start block has no children, call createElement("br") on the context
   5041    // object and append the result as the last child of start block."
   5042    if (!startBlock.hasChildNodes()) {
   5043        startBlock.appendChild(document.createElement("br"));
   5044    }
   5045 
   5046    // "Remove extraneous line breaks at the end of start block."
   5047    removeExtraneousLineBreaksAtTheEndOf(startBlock);
   5048 
   5049    // "Restore states and values from overrides."
   5050    restoreStatesAndValues(overrides);
   5051 }
   5052 
   5053 
   5054 //@}
   5055 ///// Splitting a node list's parent /////
   5056 //@{
   5057 
   5058 function splitParent(nodeList) {
   5059    // "Let original parent be the parent of the first member of node list."
   5060    var originalParent = nodeList[0].parentNode;
   5061 
   5062    // "If original parent is not editable or its parent is null, do nothing
   5063    // and abort these steps."
   5064    if (!isEditable(originalParent)
   5065    || !originalParent.parentNode) {
   5066        return;
   5067    }
   5068 
   5069    // "If the first child of original parent is in node list, remove
   5070    // extraneous line breaks before original parent."
   5071    if (nodeList.indexOf(originalParent.firstChild) != -1) {
   5072        removeExtraneousLineBreaksBefore(originalParent);
   5073    }
   5074 
   5075    // "If the first child of original parent is in node list, and original
   5076    // parent follows a line break, set follows line break to true. Otherwise,
   5077    // set follows line break to false."
   5078    var followsLineBreak_ = nodeList.indexOf(originalParent.firstChild) != -1
   5079        && followsLineBreak(originalParent);
   5080 
   5081    // "If the last child of original parent is in node list, and original
   5082    // parent precedes a line break, set precedes line break to true.
   5083    // Otherwise, set precedes line break to false."
   5084    var precedesLineBreak_ = nodeList.indexOf(originalParent.lastChild) != -1
   5085        && precedesLineBreak(originalParent);
   5086 
   5087    // "If the first child of original parent is not in node list, but its last
   5088    // child is:"
   5089    if (nodeList.indexOf(originalParent.firstChild) == -1
   5090    && nodeList.indexOf(originalParent.lastChild) != -1) {
   5091        // "For each node in node list, in reverse order, insert node into the
   5092        // parent of original parent immediately after original parent,
   5093        // preserving ranges."
   5094        for (var i = nodeList.length - 1; i >= 0; i--) {
   5095            movePreservingRanges(nodeList[i], originalParent.parentNode, 1 + getNodeIndex(originalParent));
   5096        }
   5097 
   5098        // "If precedes line break is true, and the last member of node list
   5099        // does not precede a line break, call createElement("br") on the
   5100        // context object and insert the result immediately after the last
   5101        // member of node list."
   5102        if (precedesLineBreak_
   5103        && !precedesLineBreak(nodeList[nodeList.length - 1])) {
   5104            nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling);
   5105        }
   5106 
   5107        // "Remove extraneous line breaks at the end of original parent."
   5108        removeExtraneousLineBreaksAtTheEndOf(originalParent);
   5109 
   5110        // "Abort these steps."
   5111        return;
   5112    }
   5113 
   5114    // "If the first child of original parent is not in node list:"
   5115    if (nodeList.indexOf(originalParent.firstChild) == -1) {
   5116        // "Let cloned parent be the result of calling cloneNode(false) on
   5117        // original parent."
   5118        var clonedParent = originalParent.cloneNode(false);
   5119 
   5120        // "If original parent has an id attribute, unset it."
   5121        originalParent.removeAttribute("id");
   5122 
   5123        // "Insert cloned parent into the parent of original parent immediately
   5124        // before original parent."
   5125        originalParent.parentNode.insertBefore(clonedParent, originalParent);
   5126 
   5127        // "While the previousSibling of the first member of node list is not
   5128        // null, append the first child of original parent as the last child of
   5129        // cloned parent, preserving ranges."
   5130        while (nodeList[0].previousSibling) {
   5131            movePreservingRanges(originalParent.firstChild, clonedParent, clonedParent.childNodes.length);
   5132        }
   5133    }
   5134 
   5135    // "For each node in node list, insert node into the parent of original
   5136    // parent immediately before original parent, preserving ranges."
   5137    for (var i = 0; i < nodeList.length; i++) {
   5138        movePreservingRanges(nodeList[i], originalParent.parentNode, getNodeIndex(originalParent));
   5139    }
   5140 
   5141    // "If follows line break is true, and the first member of node list does
   5142    // not follow a line break, call createElement("br") on the context object
   5143    // and insert the result immediately before the first member of node list."
   5144    if (followsLineBreak_
   5145    && !followsLineBreak(nodeList[0])) {
   5146        nodeList[0].parentNode.insertBefore(document.createElement("br"), nodeList[0]);
   5147    }
   5148 
   5149    // "If the last member of node list is an inline node other than a br, and
   5150    // the first child of original parent is a br, and original parent is not
   5151    // an inline node, remove the first child of original parent from original
   5152    // parent."
   5153    if (isInlineNode(nodeList[nodeList.length - 1])
   5154    && !isHtmlElement(nodeList[nodeList.length - 1], "br")
   5155    && isHtmlElement(originalParent.firstChild, "br")
   5156    && !isInlineNode(originalParent)) {
   5157        originalParent.removeChild(originalParent.firstChild);
   5158    }
   5159 
   5160    // "If original parent has no children:"
   5161    if (!originalParent.hasChildNodes()) {
   5162        // "Remove original parent from its parent."
   5163        originalParent.parentNode.removeChild(originalParent);
   5164 
   5165        // "If precedes line break is true, and the last member of node list
   5166        // does not precede a line break, call createElement("br") on the
   5167        // context object and insert the result immediately after the last
   5168        // member of node list."
   5169        if (precedesLineBreak_
   5170        && !precedesLineBreak(nodeList[nodeList.length - 1])) {
   5171            nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling);
   5172        }
   5173 
   5174    // "Otherwise, remove extraneous line breaks before original parent."
   5175    } else {
   5176        removeExtraneousLineBreaksBefore(originalParent);
   5177    }
   5178 
   5179    // "If node list's last member's nextSibling is null, but its parent is not
   5180    // null, remove extraneous line breaks at the end of node list's last
   5181    // member's parent."
   5182    if (!nodeList[nodeList.length - 1].nextSibling
   5183    && nodeList[nodeList.length - 1].parentNode) {
   5184        removeExtraneousLineBreaksAtTheEndOf(nodeList[nodeList.length - 1].parentNode);
   5185    }
   5186 }
   5187 
   5188 // "To remove a node node while preserving its descendants, split the parent of
   5189 // node's children if it has any. If it has no children, instead remove it from
   5190 // its parent."
   5191 function removePreservingDescendants(node) {
   5192    if (node.hasChildNodes()) {
   5193        splitParent([].slice.call(node.childNodes));
   5194    } else {
   5195        node.parentNode.removeChild(node);
   5196    }
   5197 }
   5198 
   5199 
   5200 //@}
   5201 ///// Canonical space sequences /////
   5202 //@{
   5203 
   5204 function canonicalSpaceSequence(n, nonBreakingStart, nonBreakingEnd) {
   5205    // "If n is zero, return the empty string."
   5206    if (n == 0) {
   5207        return "";
   5208    }
   5209 
   5210    // "If n is one and both non-breaking start and non-breaking end are false,
   5211    // return a single space (U+0020)."
   5212    if (n == 1 && !nonBreakingStart && !nonBreakingEnd) {
   5213        return " ";
   5214    }
   5215 
   5216    // "If n is one, return a single non-breaking space (U+00A0)."
   5217    if (n == 1) {
   5218        return "\xa0";
   5219    }
   5220 
   5221    // "Let buffer be the empty string."
   5222    var buffer = "";
   5223 
   5224    // "If non-breaking start is true, let repeated pair be U+00A0 U+0020.
   5225    // Otherwise, let it be U+0020 U+00A0."
   5226    var repeatedPair;
   5227    if (nonBreakingStart) {
   5228        repeatedPair = "\xa0 ";
   5229    } else {
   5230        repeatedPair = " \xa0";
   5231    }
   5232 
   5233    // "While n is greater than three, append repeated pair to buffer and
   5234    // subtract two from n."
   5235    while (n > 3) {
   5236        buffer += repeatedPair;
   5237        n -= 2;
   5238    }
   5239 
   5240    // "If n is three, append a three-element string to buffer depending on
   5241    // non-breaking start and non-breaking end:"
   5242    if (n == 3) {
   5243        buffer +=
   5244            !nonBreakingStart && !nonBreakingEnd ? " \xa0 "
   5245            : nonBreakingStart && !nonBreakingEnd ? "\xa0\xa0 "
   5246            : !nonBreakingStart && nonBreakingEnd ? " \xa0\xa0"
   5247            : nonBreakingStart && nonBreakingEnd ? "\xa0 \xa0"
   5248            : "impossible";
   5249 
   5250    // "Otherwise, append a two-element string to buffer depending on
   5251    // non-breaking start and non-breaking end:"
   5252    } else {
   5253        buffer +=
   5254            !nonBreakingStart && !nonBreakingEnd ? "\xa0 "
   5255            : nonBreakingStart && !nonBreakingEnd ? "\xa0 "
   5256            : !nonBreakingStart && nonBreakingEnd ? " \xa0"
   5257            : nonBreakingStart && nonBreakingEnd ? "\xa0\xa0"
   5258            : "impossible";
   5259    }
   5260 
   5261    // "Return buffer."
   5262    return buffer;
   5263 }
   5264 
   5265 function canonicalizeWhitespace(node, offset, fixCollapsedSpace) {
   5266    if (fixCollapsedSpace === undefined) {
   5267        // "an optional boolean argument fix collapsed space that defaults to
   5268        // true"
   5269        fixCollapsedSpace = true;
   5270    }
   5271 
   5272    // "If node is neither editable nor an editing host, abort these steps."
   5273    if (!isEditable(node) && !isEditingHost(node)) {
   5274        return;
   5275    }
   5276 
   5277    // "Let start node equal node and let start offset equal offset."
   5278    var startNode = node;
   5279    var startOffset = offset;
   5280 
   5281    // "Repeat the following steps:"
   5282    while (true) {
   5283        // "If start node has a child in the same editing host with index start
   5284        // offset minus one, set start node to that child, then set start
   5285        // offset to start node's length."
   5286        if (0 <= startOffset - 1
   5287        && inSameEditingHost(startNode, startNode.childNodes[startOffset - 1])) {
   5288            startNode = startNode.childNodes[startOffset - 1];
   5289            startOffset = getNodeLength(startNode);
   5290 
   5291        // "Otherwise, if start offset is zero and start node does not follow a
   5292        // line break and start node's parent is in the same editing host, set
   5293        // start offset to start node's index, then set start node to its
   5294        // parent."
   5295        } else if (startOffset == 0
   5296        && !followsLineBreak(startNode)
   5297        && inSameEditingHost(startNode, startNode.parentNode)) {
   5298            startOffset = getNodeIndex(startNode);
   5299            startNode = startNode.parentNode;
   5300 
   5301        // "Otherwise, if start node is a Text node and its parent's resolved
   5302        // value for "white-space" is neither "pre" nor "pre-wrap" and start
   5303        // offset is not zero and the (start offset − 1)st element of start
   5304        // node's data is a space (0x0020) or non-breaking space (0x00A0),
   5305        // subtract one from start offset."
   5306        } else if (startNode.nodeType == Node.TEXT_NODE
   5307        && ["pre", "pre-wrap"].indexOf(getComputedStyle(startNode.parentNode).whiteSpace) == -1
   5308        && startOffset != 0
   5309        && /[ \xa0]/.test(startNode.data[startOffset - 1])) {
   5310            startOffset--;
   5311 
   5312        // "Otherwise, break from this loop."
   5313        } else {
   5314            break;
   5315        }
   5316    }
   5317 
   5318    // "Let end node equal start node and end offset equal start offset."
   5319    var endNode = startNode;
   5320    var endOffset = startOffset;
   5321 
   5322    // "Let length equal zero."
   5323    var length = 0;
   5324 
   5325    // "Let collapse spaces be true if start offset is zero and start node
   5326    // follows a line break, otherwise false."
   5327    var collapseSpaces = startOffset == 0 && followsLineBreak(startNode);
   5328 
   5329    // "Repeat the following steps:"
   5330    while (true) {
   5331        // "If end node has a child in the same editing host with index end
   5332        // offset, set end node to that child, then set end offset to zero."
   5333        if (endOffset < endNode.childNodes.length
   5334        && inSameEditingHost(endNode, endNode.childNodes[endOffset])) {
   5335            endNode = endNode.childNodes[endOffset];
   5336            endOffset = 0;
   5337 
   5338        // "Otherwise, if end offset is end node's length and end node does not
   5339        // precede a line break and end node's parent is in the same editing
   5340        // host, set end offset to one plus end node's index, then set end node
   5341        // to its parent."
   5342        } else if (endOffset == getNodeLength(endNode)
   5343        && !precedesLineBreak(endNode)
   5344        && inSameEditingHost(endNode, endNode.parentNode)) {
   5345            endOffset = 1 + getNodeIndex(endNode);
   5346            endNode = endNode.parentNode;
   5347 
   5348        // "Otherwise, if end node is a Text node and its parent's resolved
   5349        // value for "white-space" is neither "pre" nor "pre-wrap" and end
   5350        // offset is not end node's length and the end offsetth element of
   5351        // end node's data is a space (0x0020) or non-breaking space (0x00A0):"
   5352        } else if (endNode.nodeType == Node.TEXT_NODE
   5353        && ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).whiteSpace) == -1
   5354        && endOffset != getNodeLength(endNode)
   5355        && /[ \xa0]/.test(endNode.data[endOffset])) {
   5356            // "If fix collapsed space is true, and collapse spaces is true,
   5357            // and the end offsetth code unit of end node's data is a space
   5358            // (0x0020): call deleteData(end offset, 1) on end node, then
   5359            // continue this loop from the beginning."
   5360            if (fixCollapsedSpace
   5361            && collapseSpaces
   5362            && " " == endNode.data[endOffset]) {
   5363                endNode.deleteData(endOffset, 1);
   5364                continue;
   5365            }
   5366 
   5367            // "Set collapse spaces to true if the end offsetth element of end
   5368            // node's data is a space (0x0020), false otherwise."
   5369            collapseSpaces = " " == endNode.data[endOffset];
   5370 
   5371            // "Add one to end offset."
   5372            endOffset++;
   5373 
   5374            // "Add one to length."
   5375            length++;
   5376 
   5377        // "Otherwise, break from this loop."
   5378        } else {
   5379            break;
   5380        }
   5381    }
   5382 
   5383    // "If fix collapsed space is true, then while (start node, start offset)
   5384    // is before (end node, end offset):"
   5385    if (fixCollapsedSpace) {
   5386        while (getPosition(startNode, startOffset, endNode, endOffset) == "before") {
   5387            // "If end node has a child in the same editing host with index end
   5388            // offset − 1, set end node to that child, then set end offset to end
   5389            // node's length."
   5390            if (0 <= endOffset - 1
   5391            && endOffset - 1 < endNode.childNodes.length
   5392            && inSameEditingHost(endNode, endNode.childNodes[endOffset - 1])) {
   5393                endNode = endNode.childNodes[endOffset - 1];
   5394                endOffset = getNodeLength(endNode);
   5395 
   5396            // "Otherwise, if end offset is zero and end node's parent is in the
   5397            // same editing host, set end offset to end node's index, then set end
   5398            // node to its parent."
   5399            } else if (endOffset == 0
   5400            && inSameEditingHost(endNode, endNode.parentNode)) {
   5401                endOffset = getNodeIndex(endNode);
   5402                endNode = endNode.parentNode;
   5403 
   5404            // "Otherwise, if end node is a Text node and its parent's resolved
   5405            // value for "white-space" is neither "pre" nor "pre-wrap" and end
   5406            // offset is end node's length and the last code unit of end node's
   5407            // data is a space (0x0020) and end node precedes a line break:"
   5408            } else if (endNode.nodeType == Node.TEXT_NODE
   5409            && ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).whiteSpace) == -1
   5410            && endOffset == getNodeLength(endNode)
   5411            && endNode.data[endNode.data.length - 1] == " "
   5412            && precedesLineBreak(endNode)) {
   5413                // "Subtract one from end offset."
   5414                endOffset--;
   5415 
   5416                // "Subtract one from length."
   5417                length--;
   5418 
   5419                // "Call deleteData(end offset, 1) on end node."
   5420                endNode.deleteData(endOffset, 1);
   5421 
   5422            // "Otherwise, break from this loop."
   5423            } else {
   5424                break;
   5425            }
   5426        }
   5427    }
   5428 
   5429    // "Let replacement whitespace be the canonical space sequence of length
   5430    // length. non-breaking start is true if start offset is zero and start
   5431    // node follows a line break, and false otherwise. non-breaking end is true
   5432    // if end offset is end node's length and end node precedes a line break,
   5433    // and false otherwise."
   5434    var replacementWhitespace = canonicalSpaceSequence(length,
   5435        startOffset == 0 && followsLineBreak(startNode),
   5436        endOffset == getNodeLength(endNode) && precedesLineBreak(endNode));
   5437 
   5438    // "While (start node, start offset) is before (end node, end offset):"
   5439    while (getPosition(startNode, startOffset, endNode, endOffset) == "before") {
   5440        // "If start node has a child with index start offset, set start node
   5441        // to that child, then set start offset to zero."
   5442        if (startOffset < startNode.childNodes.length) {
   5443            startNode = startNode.childNodes[startOffset];
   5444            startOffset = 0;
   5445 
   5446        // "Otherwise, if start node is not a Text node or if start offset is
   5447        // start node's length, set start offset to one plus start node's
   5448        // index, then set start node to its parent."
   5449        } else if (startNode.nodeType != Node.TEXT_NODE
   5450        || startOffset == getNodeLength(startNode)) {
   5451            startOffset = 1 + getNodeIndex(startNode);
   5452            startNode = startNode.parentNode;
   5453 
   5454        // "Otherwise:"
   5455        } else {
   5456            // "Remove the first element from replacement whitespace, and let
   5457            // element be that element."
   5458            var element = replacementWhitespace[0];
   5459            replacementWhitespace = replacementWhitespace.slice(1);
   5460 
   5461            // "If element is not the same as the start offsetth element of
   5462            // start node's data:"
   5463            if (element != startNode.data[startOffset]) {
   5464                // "Call insertData(start offset, element) on start node."
   5465                startNode.insertData(startOffset, element);
   5466 
   5467                // "Call deleteData(start offset + 1, 1) on start node."
   5468                startNode.deleteData(startOffset + 1, 1);
   5469            }
   5470 
   5471            // "Add one to start offset."
   5472            startOffset++;
   5473        }
   5474    }
   5475 }
   5476 
   5477 
   5478 //@}
   5479 ///// Indenting and outdenting /////
   5480 //@{
   5481 
   5482 function indentNodes(nodeList) {
   5483    // "If node list is empty, do nothing and abort these steps."
   5484    if (!nodeList.length) {
   5485        return;
   5486    }
   5487 
   5488    // "Let first node be the first member of node list."
   5489    var firstNode = nodeList[0];
   5490 
   5491    // "If first node's parent is an ol or ul:"
   5492    if (isHtmlElement(firstNode.parentNode, ["OL", "UL"])) {
   5493        // "Let tag be the local name of the parent of first node."
   5494        var tag = firstNode.parentNode.tagName;
   5495 
   5496        // "Wrap node list, with sibling criteria returning true for an HTML
   5497        // element with local name tag and false otherwise, and new parent
   5498        // instructions returning the result of calling createElement(tag) on
   5499        // the ownerDocument of first node."
   5500        wrap(nodeList,
   5501            function(node) { return isHtmlElement(node, tag) },
   5502            function() { return firstNode.ownerDocument.createElement(tag) });
   5503 
   5504        // "Abort these steps."
   5505        return;
   5506    }
   5507 
   5508    // "Wrap node list, with sibling criteria returning true for a simple
   5509    // indentation element and false otherwise, and new parent instructions
   5510    // returning the result of calling createElement("blockquote") on the
   5511    // ownerDocument of first node. Let new parent be the result."
   5512    var newParent = wrap(nodeList,
   5513        function(node) { return isSimpleIndentationElement(node) },
   5514        function() { return firstNode.ownerDocument.createElement("blockquote") });
   5515 
   5516    // "Fix disallowed ancestors of new parent."
   5517    fixDisallowedAncestors(newParent);
   5518 }
   5519 
   5520 function outdentNode(node) {
   5521    // "If node is not editable, abort these steps."
   5522    if (!isEditable(node)) {
   5523        return;
   5524    }
   5525 
   5526    // "If node is a simple indentation element, remove node, preserving its
   5527    // descendants.  Then abort these steps."
   5528    if (isSimpleIndentationElement(node)) {
   5529        removePreservingDescendants(node);
   5530        return;
   5531    }
   5532 
   5533    // "If node is an indentation element:"
   5534    if (isIndentationElement(node)) {
   5535        // "Unset the dir attribute of node, if any."
   5536        node.removeAttribute("dir");
   5537 
   5538        // "Unset the margin, padding, and border CSS properties of node."
   5539        node.style.margin = "";
   5540        node.style.padding = "";
   5541        node.style.border = "";
   5542        if (node.getAttribute("style") == ""
   5543        // Crazy WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=68551
   5544        || node.getAttribute("style") == "border-width: initial; border-color: initial; ") {
   5545            node.removeAttribute("style");
   5546        }
   5547 
   5548        // "Set the tag name of node to "div"."
   5549        setTagName(node, "div");
   5550 
   5551        // "Abort these steps."
   5552        return;
   5553    }
   5554 
   5555    // "Let current ancestor be node's parent."
   5556    var currentAncestor = node.parentNode;
   5557 
   5558    // "Let ancestor list be a list of nodes, initially empty."
   5559    var ancestorList = [];
   5560 
   5561    // "While current ancestor is an editable Element that is neither a simple
   5562    // indentation element nor an ol nor a ul, append current ancestor to
   5563    // ancestor list and then set current ancestor to its parent."
   5564    while (isEditable(currentAncestor)
   5565    && currentAncestor.nodeType == Node.ELEMENT_NODE
   5566    && !isSimpleIndentationElement(currentAncestor)
   5567    && !isHtmlElement(currentAncestor, ["ol", "ul"])) {
   5568        ancestorList.push(currentAncestor);
   5569        currentAncestor = currentAncestor.parentNode;
   5570    }
   5571 
   5572    // "If current ancestor is not an editable simple indentation element:"
   5573    if (!isEditable(currentAncestor)
   5574    || !isSimpleIndentationElement(currentAncestor)) {
   5575        // "Let current ancestor be node's parent."
   5576        currentAncestor = node.parentNode;
   5577 
   5578        // "Let ancestor list be the empty list."
   5579        ancestorList = [];
   5580 
   5581        // "While current ancestor is an editable Element that is neither an
   5582        // indentation element nor an ol nor a ul, append current ancestor to
   5583        // ancestor list and then set current ancestor to its parent."
   5584        while (isEditable(currentAncestor)
   5585        && currentAncestor.nodeType == Node.ELEMENT_NODE
   5586        && !isIndentationElement(currentAncestor)
   5587        && !isHtmlElement(currentAncestor, ["ol", "ul"])) {
   5588            ancestorList.push(currentAncestor);
   5589            currentAncestor = currentAncestor.parentNode;
   5590        }
   5591    }
   5592 
   5593    // "If node is an ol or ul and current ancestor is not an editable
   5594    // indentation element:"
   5595    if (isHtmlElement(node, ["OL", "UL"])
   5596    && (!isEditable(currentAncestor)
   5597    || !isIndentationElement(currentAncestor))) {
   5598        // "Unset the reversed, start, and type attributes of node, if any are
   5599        // set."
   5600        node.removeAttribute("reversed");
   5601        node.removeAttribute("start");
   5602        node.removeAttribute("type");
   5603 
   5604        // "Let children be the children of node."
   5605        var children = [].slice.call(node.childNodes);
   5606 
   5607        // "If node has attributes, and its parent is not an ol or ul, set the
   5608        // tag name of node to "div"."
   5609        if (node.attributes.length
   5610        && !isHtmlElement(node.parentNode, ["OL", "UL"])) {
   5611            setTagName(node, "div");
   5612 
   5613        // "Otherwise:"
   5614        } else {
   5615            // "Record the values of node's children, and let values be the
   5616            // result."
   5617            var values = recordValues([].slice.call(node.childNodes));
   5618 
   5619            // "Remove node, preserving its descendants."
   5620            removePreservingDescendants(node);
   5621 
   5622            // "Restore the values from values."
   5623            restoreValues(values);
   5624        }
   5625 
   5626        // "Fix disallowed ancestors of each member of children."
   5627        for (var i = 0; i < children.length; i++) {
   5628            fixDisallowedAncestors(children[i]);
   5629        }
   5630 
   5631        // "Abort these steps."
   5632        return;
   5633    }
   5634 
   5635    // "If current ancestor is not an editable indentation element, abort these
   5636    // steps."
   5637    if (!isEditable(currentAncestor)
   5638    || !isIndentationElement(currentAncestor)) {
   5639        return;
   5640    }
   5641 
   5642    // "Append current ancestor to ancestor list."
   5643    ancestorList.push(currentAncestor);
   5644 
   5645    // "Let original ancestor be current ancestor."
   5646    var originalAncestor = currentAncestor;
   5647 
   5648    // "While ancestor list is not empty:"
   5649    while (ancestorList.length) {
   5650        // "Let current ancestor be the last member of ancestor list."
   5651        //
   5652        // "Remove the last member of ancestor list."
   5653        currentAncestor = ancestorList.pop();
   5654 
   5655        // "Let target be the child of current ancestor that is equal to either
   5656        // node or the last member of ancestor list."
   5657        var target = node.parentNode == currentAncestor
   5658            ? node
   5659            : ancestorList[ancestorList.length - 1];
   5660 
   5661        // "If target is an inline node that is not a br, and its nextSibling
   5662        // is a br, remove target's nextSibling from its parent."
   5663        if (isInlineNode(target)
   5664        && !isHtmlElement(target, "BR")
   5665        && isHtmlElement(target.nextSibling, "BR")) {
   5666            target.parentNode.removeChild(target.nextSibling);
   5667        }
   5668 
   5669        // "Let preceding siblings be the preceding siblings of target, and let
   5670        // following siblings be the following siblings of target."
   5671        var precedingSiblings = [].slice.call(currentAncestor.childNodes, 0, getNodeIndex(target));
   5672        var followingSiblings = [].slice.call(currentAncestor.childNodes, 1 + getNodeIndex(target));
   5673 
   5674        // "Indent preceding siblings."
   5675        indentNodes(precedingSiblings);
   5676 
   5677        // "Indent following siblings."
   5678        indentNodes(followingSiblings);
   5679    }
   5680 
   5681    // "Outdent original ancestor."
   5682    outdentNode(originalAncestor);
   5683 }
   5684 
   5685 
   5686 //@}
   5687 ///// Toggling lists /////
   5688 //@{
   5689 
   5690 function toggleLists(tagName) {
   5691    // "Let mode be "disable" if the selection's list state is tag name, and
   5692    // "enable" otherwise."
   5693    var mode = getSelectionListState() == tagName ? "disable" : "enable";
   5694 
   5695    var range = getActiveRange();
   5696    tagName = tagName.toUpperCase();
   5697 
   5698    // "Let other tag name be "ol" if tag name is "ul", and "ul" if tag name is
   5699    // "ol"."
   5700    var otherTagName = tagName == "OL" ? "UL" : "OL";
   5701 
   5702    // "Let items be a list of all lis that are ancestor containers of the
   5703    // range's start and/or end node."
   5704    //
   5705    // It's annoying to get this in tree order using functional stuff without
   5706    // doing getDescendants(document), which is slow, so I do it imperatively.
   5707    var items = [];
   5708    (function(){
   5709        for (
   5710            var ancestorContainer = range.endContainer;
   5711            ancestorContainer != range.commonAncestorContainer;
   5712            ancestorContainer = ancestorContainer.parentNode
   5713        ) {
   5714            if (isHtmlElement(ancestorContainer, "li")) {
   5715                items.unshift(ancestorContainer);
   5716            }
   5717        }
   5718        for (
   5719            var ancestorContainer = range.startContainer;
   5720            ancestorContainer;
   5721            ancestorContainer = ancestorContainer.parentNode
   5722        ) {
   5723            if (isHtmlElement(ancestorContainer, "li")) {
   5724                items.unshift(ancestorContainer);
   5725            }
   5726        }
   5727    })();
   5728 
   5729    // "For each item in items, normalize sublists of item."
   5730    items.forEach(normalizeSublists);
   5731 
   5732    // "Block-extend the range, and let new range be the result."
   5733    var newRange = blockExtend(range);
   5734 
   5735    // "If mode is "enable", then let lists to convert consist of every
   5736    // editable HTML element with local name other tag name that is contained
   5737    // in new range, and for every list in lists to convert:"
   5738    if (mode == "enable") {
   5739        getAllContainedNodes(newRange, function(node) {
   5740            return isEditable(node)
   5741                && isHtmlElement(node, otherTagName);
   5742        }).forEach(function(list) {
   5743            // "If list's previousSibling or nextSibling is an editable HTML
   5744            // element with local name tag name:"
   5745            if ((isEditable(list.previousSibling) && isHtmlElement(list.previousSibling, tagName))
   5746            || (isEditable(list.nextSibling) && isHtmlElement(list.nextSibling, tagName))) {
   5747                // "Let children be list's children."
   5748                var children = [].slice.call(list.childNodes);
   5749 
   5750                // "Record the values of children, and let values be the
   5751                // result."
   5752                var values = recordValues(children);
   5753 
   5754                // "Split the parent of children."
   5755                splitParent(children);
   5756 
   5757                // "Wrap children, with sibling criteria returning true for an
   5758                // HTML element with local name tag name and false otherwise."
   5759                wrap(children, function(node) { return isHtmlElement(node, tagName) });
   5760 
   5761                // "Restore the values from values."
   5762                restoreValues(values);
   5763 
   5764            // "Otherwise, set the tag name of list to tag name."
   5765            } else {
   5766                setTagName(list, tagName);
   5767            }
   5768        });
   5769    }
   5770 
   5771    // "Let node list be a list of nodes, initially empty."
   5772    //
   5773    // "For each node node contained in new range, if node is editable; the
   5774    // last member of node list (if any) is not an ancestor of node; node
   5775    // is not an indentation element; and either node is an ol or ul, or its
   5776    // parent is an ol or ul, or it is an allowed child of "li"; then append
   5777    // node to node list."
   5778    var nodeList = getContainedNodes(newRange, function(node) {
   5779        return isEditable(node)
   5780        && !isIndentationElement(node)
   5781        && (isHtmlElement(node, ["OL", "UL"])
   5782        || isHtmlElement(node.parentNode, ["OL", "UL"])
   5783        || isAllowedChild(node, "li"));
   5784    });
   5785 
   5786    // "If mode is "enable", remove from node list any ol or ul whose parent is
   5787    // not also an ol or ul."
   5788    if (mode == "enable") {
   5789        nodeList = nodeList.filter(function(node) {
   5790            return !isHtmlElement(node, ["ol", "ul"])
   5791                || isHtmlElement(node.parentNode, ["ol", "ul"]);
   5792        });
   5793    }
   5794 
   5795    // "If mode is "disable", then while node list is not empty:"
   5796    if (mode == "disable") {
   5797        while (nodeList.length) {
   5798            // "Let sublist be an empty list of nodes."
   5799            var sublist = [];
   5800 
   5801            // "Remove the first member from node list and append it to
   5802            // sublist."
   5803            sublist.push(nodeList.shift());
   5804 
   5805            // "If the first member of sublist is an HTML element with local
   5806            // name tag name, outdent it and continue this loop from the
   5807            // beginning."
   5808            if (isHtmlElement(sublist[0], tagName)) {
   5809                outdentNode(sublist[0]);
   5810                continue;
   5811            }
   5812 
   5813            // "While node list is not empty, and the first member of node list
   5814            // is the nextSibling of the last member of sublist and is not an
   5815            // HTML element with local name tag name, remove the first member
   5816            // from node list and append it to sublist."
   5817            while (nodeList.length
   5818            && nodeList[0] == sublist[sublist.length - 1].nextSibling
   5819            && !isHtmlElement(nodeList[0], tagName)) {
   5820                sublist.push(nodeList.shift());
   5821            }
   5822 
   5823            // "Record the values of sublist, and let values be the result."
   5824            var values = recordValues(sublist);
   5825 
   5826            // "Split the parent of sublist."
   5827            splitParent(sublist);
   5828 
   5829            // "Fix disallowed ancestors of each member of sublist."
   5830            for (var i = 0; i < sublist.length; i++) {
   5831                fixDisallowedAncestors(sublist[i]);
   5832            }
   5833 
   5834            // "Restore the values from values."
   5835            restoreValues(values);
   5836        }
   5837 
   5838    // "Otherwise, while node list is not empty:"
   5839    } else {
   5840        while (nodeList.length) {
   5841            // "Let sublist be an empty list of nodes."
   5842            var sublist = [];
   5843 
   5844            // "While either sublist is empty, or node list is not empty and
   5845            // its first member is the nextSibling of sublist's last member:"
   5846            while (!sublist.length
   5847            || (nodeList.length
   5848            && nodeList[0] == sublist[sublist.length - 1].nextSibling)) {
   5849                // "If node list's first member is a p or div, set the tag name
   5850                // of node list's first member to "li", and append the result
   5851                // to sublist. Remove the first member from node list."
   5852                if (isHtmlElement(nodeList[0], ["p", "div"])) {
   5853                    sublist.push(setTagName(nodeList[0], "li"));
   5854                    nodeList.shift();
   5855 
   5856                // "Otherwise, if the first member of node list is an li or ol
   5857                // or ul, remove it from node list and append it to sublist."
   5858                } else if (isHtmlElement(nodeList[0], ["li", "ol", "ul"])) {
   5859                    sublist.push(nodeList.shift());
   5860 
   5861                // "Otherwise:"
   5862                } else {
   5863                    // "Let nodes to wrap be a list of nodes, initially empty."
   5864                    var nodesToWrap = [];
   5865 
   5866                    // "While nodes to wrap is empty, or node list is not empty
   5867                    // and its first member is the nextSibling of nodes to
   5868                    // wrap's last member and the first member of node list is
   5869                    // an inline node and the last member of nodes to wrap is
   5870                    // an inline node other than a br, remove the first member
   5871                    // from node list and append it to nodes to wrap."
   5872                    while (!nodesToWrap.length
   5873                    || (nodeList.length
   5874                    && nodeList[0] == nodesToWrap[nodesToWrap.length - 1].nextSibling
   5875                    && isInlineNode(nodeList[0])
   5876                    && isInlineNode(nodesToWrap[nodesToWrap.length - 1])
   5877                    && !isHtmlElement(nodesToWrap[nodesToWrap.length - 1], "br"))) {
   5878                        nodesToWrap.push(nodeList.shift());
   5879                    }
   5880 
   5881                    // "Wrap nodes to wrap, with new parent instructions
   5882                    // returning the result of calling createElement("li") on
   5883                    // the context object. Append the result to sublist."
   5884                    sublist.push(wrap(nodesToWrap,
   5885                        undefined,
   5886                        function() { return document.createElement("li") }));
   5887                }
   5888            }
   5889 
   5890            // "If sublist's first member's parent is an HTML element with
   5891            // local name tag name, or if every member of sublist is an ol or
   5892            // ul, continue this loop from the beginning."
   5893            if (isHtmlElement(sublist[0].parentNode, tagName)
   5894            || sublist.every(function(node) { return isHtmlElement(node, ["ol", "ul"]) })) {
   5895                continue;
   5896            }
   5897 
   5898            // "If sublist's first member's parent is an HTML element with
   5899            // local name other tag name:"
   5900            if (isHtmlElement(sublist[0].parentNode, otherTagName)) {
   5901                // "Record the values of sublist, and let values be the
   5902                // result."
   5903                var values = recordValues(sublist);
   5904 
   5905                // "Split the parent of sublist."
   5906                splitParent(sublist);
   5907 
   5908                // "Wrap sublist, with sibling criteria returning true for an
   5909                // HTML element with local name tag name and false otherwise,
   5910                // and new parent instructions returning the result of calling
   5911                // createElement(tag name) on the context object."
   5912                wrap(sublist,
   5913                    function(node) { return isHtmlElement(node, tagName) },
   5914                    function() { return document.createElement(tagName) });
   5915 
   5916                // "Restore the values from values."
   5917                restoreValues(values);
   5918 
   5919                // "Continue this loop from the beginning."
   5920                continue;
   5921            }
   5922 
   5923            // "Wrap sublist, with sibling criteria returning true for an HTML
   5924            // element with local name tag name and false otherwise, and new
   5925            // parent instructions being the following:"
   5926            // . . .
   5927            // "Fix disallowed ancestors of the previous step's result."
   5928            fixDisallowedAncestors(wrap(sublist,
   5929                function(node) { return isHtmlElement(node, tagName) },
   5930                function() {
   5931                    // "If sublist's first member's parent is not an editable
   5932                    // simple indentation element, or sublist's first member's
   5933                    // parent's previousSibling is not an editable HTML element
   5934                    // with local name tag name, call createElement(tag name)
   5935                    // on the context object and return the result."
   5936                    if (!isEditable(sublist[0].parentNode)
   5937                    || !isSimpleIndentationElement(sublist[0].parentNode)
   5938                    || !isEditable(sublist[0].parentNode.previousSibling)
   5939                    || !isHtmlElement(sublist[0].parentNode.previousSibling, tagName)) {
   5940                        return document.createElement(tagName);
   5941                    }
   5942 
   5943                    // "Let list be sublist's first member's parent's
   5944                    // previousSibling."
   5945                    var list = sublist[0].parentNode.previousSibling;
   5946 
   5947                    // "Normalize sublists of list's lastChild."
   5948                    normalizeSublists(list.lastChild);
   5949 
   5950                    // "If list's lastChild is not an editable HTML element
   5951                    // with local name tag name, call createElement(tag name)
   5952                    // on the context object, and append the result as the last
   5953                    // child of list."
   5954                    if (!isEditable(list.lastChild)
   5955                    || !isHtmlElement(list.lastChild, tagName)) {
   5956                        list.appendChild(document.createElement(tagName));
   5957                    }
   5958 
   5959                    // "Return the last child of list."
   5960                    return list.lastChild;
   5961                }
   5962            ));
   5963        }
   5964    }
   5965 }
   5966 
   5967 
   5968 //@}
   5969 ///// Justifying the selection /////
   5970 //@{
   5971 
   5972 function justifySelection(alignment) {
   5973    // "Block-extend the active range, and let new range be the result."
   5974    var newRange = blockExtend(globalRange);
   5975 
   5976    // "Let element list be a list of all editable Elements contained in new
   5977    // range that either has an attribute in the HTML namespace whose local
   5978    // name is "align", or has a style attribute that sets "text-align", or is
   5979    // a center."
   5980    var elementList = getAllContainedNodes(newRange, function(node) {
   5981        return node.nodeType == Node.ELEMENT_NODE
   5982            && isEditable(node)
   5983            // Ignoring namespaces here
   5984            && (
   5985                node.hasAttribute("align")
   5986                || node.style.textAlign != ""
   5987                || isHtmlElement(node, "center")
   5988            );
   5989    });
   5990 
   5991    // "For each element in element list:"
   5992    for (var i = 0; i < elementList.length; i++) {
   5993        var element = elementList[i];
   5994 
   5995        // "If element has an attribute in the HTML namespace whose local name
   5996        // is "align", remove that attribute."
   5997        element.removeAttribute("align");
   5998 
   5999        // "Unset the CSS property "text-align" on element, if it's set by a
   6000        // style attribute."
   6001        element.style.textAlign = "";
   6002        if (element.getAttribute("style") == "") {
   6003            element.removeAttribute("style");
   6004        }
   6005 
   6006        // "If element is a div or span or center with no attributes, remove
   6007        // it, preserving its descendants."
   6008        if (isHtmlElement(element, ["div", "span", "center"])
   6009        && !element.attributes.length) {
   6010            removePreservingDescendants(element);
   6011        }
   6012 
   6013        // "If element is a center with one or more attributes, set the tag
   6014        // name of element to "div"."
   6015        if (isHtmlElement(element, "center")
   6016        && element.attributes.length) {
   6017            setTagName(element, "div");
   6018        }
   6019    }
   6020 
   6021    // "Block-extend the active range, and let new range be the result."
   6022    newRange = blockExtend(globalRange);
   6023 
   6024    // "Let node list be a list of nodes, initially empty."
   6025    var nodeList = [];
   6026 
   6027    // "For each node node contained in new range, append node to node list if
   6028    // the last member of node list (if any) is not an ancestor of node; node
   6029    // is editable; node is an allowed child of "div"; and node's alignment
   6030    // value is not alignment."
   6031    nodeList = getContainedNodes(newRange, function(node) {
   6032        return isEditable(node)
   6033            && isAllowedChild(node, "div")
   6034            && getAlignmentValue(node) != alignment;
   6035    });
   6036 
   6037    // "While node list is not empty:"
   6038    while (nodeList.length) {
   6039        // "Let sublist be a list of nodes, initially empty."
   6040        var sublist = [];
   6041 
   6042        // "Remove the first member of node list and append it to sublist."
   6043        sublist.push(nodeList.shift());
   6044 
   6045        // "While node list is not empty, and the first member of node list is
   6046        // the nextSibling of the last member of sublist, remove the first
   6047        // member of node list and append it to sublist."
   6048        while (nodeList.length
   6049        && nodeList[0] == sublist[sublist.length - 1].nextSibling) {
   6050            sublist.push(nodeList.shift());
   6051        }
   6052 
   6053        // "Wrap sublist. Sibling criteria returns true for any div that has
   6054        // one or both of the following two attributes and no other attributes,
   6055        // and false otherwise:"
   6056        //
   6057        //   * "An align attribute whose value is an ASCII case-insensitive
   6058        //     match for alignment.
   6059        //   * "A style attribute which sets exactly one CSS property
   6060        //     (including unrecognized or invalid attributes), which is
   6061        //     "text-align", which is set to alignment.
   6062        //
   6063        // "New parent instructions are to call createElement("div") on the
   6064        // context object, then set its CSS property "text-align" to alignment
   6065        // and return the result."
   6066        wrap(sublist,
   6067            function(node) {
   6068                return isHtmlElement(node, "div")
   6069                    && [].every.call(node.attributes, function(attr) {
   6070                        return (attr.name == "align" && attr.value.toLowerCase() == alignment)
   6071                            || (attr.name == "style" && node.style.length == 1 && node.style.textAlign == alignment);
   6072                    });
   6073            },
   6074            function() {
   6075                var newParent = document.createElement("div");
   6076                newParent.setAttribute("style", "text-align: " + alignment);
   6077                return newParent;
   6078            }
   6079        );
   6080    }
   6081 }
   6082 
   6083 
   6084 //@}
   6085 ///// Automatic linking /////
   6086 //@{
   6087 // "An autolinkable URL is a string of the following form:"
   6088 var autolinkableUrlRegexp =
   6089    // "Either a string matching the scheme pattern from RFC 3986 section 3.1
   6090    // followed by the literal string ://, or the literal string mailto:;
   6091    // followed by"
   6092    //
   6093    // From the RFC: scheme      = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
   6094    "([a-zA-Z][a-zA-Z0-9+.-]*://|mailto:)"
   6095    // "Zero or more characters other than space characters; followed by"
   6096    + "[^ \t\n\f\r]*"
   6097    // "A character that is not one of the ASCII characters !"'(),-.:;<>[]`{}."
   6098    + "[^!\"'(),\\-.:;<>[\\]`{}]";
   6099 
   6100 // "A valid e-mail address is a string that matches the ABNF production 1*(
   6101 // atext / "." ) "@" ldh-str *( "." ldh-str ) where atext is defined in RFC
   6102 // 5322 section 3.2.3, and ldh-str is defined in RFC 1034 section 3.5."
   6103 //
   6104 // atext: ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" /
   6105 // "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~"
   6106 //
   6107 //<ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
   6108 //<let-dig-hyp> ::= <let-dig> | "-"
   6109 //<let-dig> ::= <letter> | <digit>
   6110 var validEmailRegexp =
   6111    "[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~.]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*";
   6112 
   6113 function autolink(node, endOffset) {
   6114    // "While (node, end offset)'s previous equivalent point is not null, set
   6115    // it to its previous equivalent point."
   6116    while (getPreviousEquivalentPoint(node, endOffset)) {
   6117        var prev = getPreviousEquivalentPoint(node, endOffset);
   6118        node = prev[0];
   6119        endOffset = prev[1];
   6120    }
   6121 
   6122    // "If node is not a Text node, or has an a ancestor, do nothing and abort
   6123    // these steps."
   6124    if (node.nodeType != Node.TEXT_NODE
   6125    || getAncestors(node).some(function(ancestor) { return isHtmlElement(ancestor, "a") })) {
   6126        return;
   6127    }
   6128 
   6129    // "Let search be the largest substring of node's data whose end is end
   6130    // offset and that contains no space characters."
   6131    var search = /[^ \t\n\f\r]*$/.exec(node.substringData(0, endOffset))[0];
   6132 
   6133    // "If some substring of search is an autolinkable URL:"
   6134    if (new RegExp(autolinkableUrlRegexp).test(search)) {
   6135        // "While there is no substring of node's data ending at end offset
   6136        // that is an autolinkable URL, decrement end offset."
   6137        while (!(new RegExp(autolinkableUrlRegexp + "$").test(node.substringData(0, endOffset)))) {
   6138            endOffset--;
   6139        }
   6140 
   6141        // "Let start offset be the start index of the longest substring of
   6142        // node's data that is an autolinkable URL ending at end offset."
   6143        var startOffset = new RegExp(autolinkableUrlRegexp + "$").exec(node.substringData(0, endOffset)).index;
   6144 
   6145        // "Let href be the substring of node's data starting at start offset
   6146        // and ending at end offset."
   6147        var href = node.substringData(startOffset, endOffset - startOffset);
   6148 
   6149    // "Otherwise, if some substring of search is a valid e-mail address:"
   6150    } else if (new RegExp(validEmailRegexp).test(search)) {
   6151        // "While there is no substring of node's data ending at end offset
   6152        // that is a valid e-mail address, decrement end offset."
   6153        while (!(new RegExp(validEmailRegexp + "$").test(node.substringData(0, endOffset)))) {
   6154            endOffset--;
   6155        }
   6156 
   6157        // "Let start offset be the start index of the longest substring of
   6158        // node's data that is a valid e-mail address ending at end offset."
   6159        var startOffset = new RegExp(validEmailRegexp + "$").exec(node.substringData(0, endOffset)).index;
   6160 
   6161        // "Let href be "mailto:" concatenated with the substring of node's
   6162        // data starting at start offset and ending at end offset."
   6163        var href = "mailto:" + node.substringData(startOffset, endOffset - startOffset);
   6164 
   6165    // "Otherwise, do nothing and abort these steps."
   6166    } else {
   6167        return;
   6168    }
   6169 
   6170    // "Let original range be the active range."
   6171    var originalRange = getActiveRange();
   6172 
   6173    // "Create a new range with start (node, start offset) and end (node, end
   6174    // offset), and set the context object's selection's range to it."
   6175    var newRange = document.createRange();
   6176    newRange.setStart(node, startOffset);
   6177    newRange.setEnd(node, endOffset);
   6178    getSelection().removeAllRanges();
   6179    getSelection().addRange(newRange);
   6180    globalRange = newRange;
   6181 
   6182    // "Take the action for "createLink", with value equal to href."
   6183    commands.createlink.action(href);
   6184 
   6185    // "Set the context object's selection's range to original range."
   6186    getSelection().removeAllRanges();
   6187    getSelection().addRange(originalRange);
   6188    globalRange = originalRange;
   6189 }
   6190 //@}
   6191 ///// The delete command /////
   6192 //@{
   6193 commands["delete"] = {
   6194    preservesOverrides: true,
   6195    action: function() {
   6196        // "If the active range is not collapsed, delete the selection and
   6197        // return true."
   6198        if (!getActiveRange().collapsed) {
   6199            deleteSelection();
   6200            return true;
   6201        }
   6202 
   6203        // "Canonicalize whitespace at the active range's start."
   6204        canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset);
   6205 
   6206        // "Let node and offset be the active range's start node and offset."
   6207        var node = getActiveRange().startContainer;
   6208        var offset = getActiveRange().startOffset;
   6209 
   6210        // "Repeat the following steps:"
   6211        while (true) {
   6212            // "If offset is zero and node's previousSibling is an editable
   6213            // invisible node, remove node's previousSibling from its parent."
   6214            if (offset == 0
   6215            && isEditable(node.previousSibling)
   6216            && isInvisible(node.previousSibling)) {
   6217                node.parentNode.removeChild(node.previousSibling);
   6218 
   6219            // "Otherwise, if node has a child with index offset − 1 and that
   6220            // child is an editable invisible node, remove that child from
   6221            // node, then subtract one from offset."
   6222            } else if (0 <= offset - 1
   6223            && offset - 1 < node.childNodes.length
   6224            && isEditable(node.childNodes[offset - 1])
   6225            && isInvisible(node.childNodes[offset - 1])) {
   6226                node.removeChild(node.childNodes[offset - 1]);
   6227                offset--;
   6228 
   6229            // "Otherwise, if offset is zero and node is an inline node, or if
   6230            // node is an invisible node, set offset to the index of node, then
   6231            // set node to its parent."
   6232            } else if ((offset == 0
   6233            && isInlineNode(node))
   6234            || isInvisible(node)) {
   6235                offset = getNodeIndex(node);
   6236                node = node.parentNode;
   6237 
   6238            // "Otherwise, if node has a child with index offset − 1 and that
   6239            // child is an editable a, remove that child from node, preserving
   6240            // its descendants. Then return true."
   6241            } else if (0 <= offset - 1
   6242            && offset - 1 < node.childNodes.length
   6243            && isEditable(node.childNodes[offset - 1])
   6244            && isHtmlElement(node.childNodes[offset - 1], "a")) {
   6245                removePreservingDescendants(node.childNodes[offset - 1]);
   6246                return true;
   6247 
   6248            // "Otherwise, if node has a child with index offset − 1 and that
   6249            // child is not a block node or a br or an img, set node to that
   6250            // child, then set offset to the length of node."
   6251            } else if (0 <= offset - 1
   6252            && offset - 1 < node.childNodes.length
   6253            && !isBlockNode(node.childNodes[offset - 1])
   6254            && !isHtmlElement(node.childNodes[offset - 1], ["br", "img"])) {
   6255                node = node.childNodes[offset - 1];
   6256                offset = getNodeLength(node);
   6257 
   6258            // "Otherwise, break from this loop."
   6259            } else {
   6260                break;
   6261            }
   6262        }
   6263 
   6264        // "If node is a Text node and offset is not zero, or if node is a
   6265        // block node that has a child with index offset − 1 and that child is
   6266        // a br or hr or img:"
   6267        if ((node.nodeType == Node.TEXT_NODE
   6268        && offset != 0)
   6269        || (isBlockNode(node)
   6270        && 0 <= offset - 1
   6271        && offset - 1 < node.childNodes.length
   6272        && isHtmlElement(node.childNodes[offset - 1], ["br", "hr", "img"]))) {
   6273            // "Call collapse(node, offset) on the context object's Selection."
   6274            getSelection().collapse(node, offset);
   6275            getActiveRange().setEnd(node, offset);
   6276 
   6277            // "Call extend(node, offset − 1) on the context object's
   6278            // Selection."
   6279            getSelection().extend(node, offset - 1);
   6280            getActiveRange().setStart(node, offset - 1);
   6281 
   6282            // "Delete the selection."
   6283            deleteSelection();
   6284 
   6285            // "Return true."
   6286            return true;
   6287        }
   6288 
   6289        // "If node is an inline node, return true."
   6290        if (isInlineNode(node)) {
   6291            return true;
   6292        }
   6293 
   6294        // "If node is an li or dt or dd and is the first child of its parent,
   6295        // and offset is zero:"
   6296        if (isHtmlElement(node, ["li", "dt", "dd"])
   6297        && node == node.parentNode.firstChild
   6298        && offset == 0) {
   6299            // "Let items be a list of all lis that are ancestors of node."
   6300            //
   6301            // Remember, must be in tree order.
   6302            var items = [];
   6303            for (var ancestor = node.parentNode; ancestor; ancestor = ancestor.parentNode) {
   6304                if (isHtmlElement(ancestor, "li")) {
   6305                    items.unshift(ancestor);
   6306                }
   6307            }
   6308 
   6309            // "Normalize sublists of each item in items."
   6310            for (var i = 0; i < items.length; i++) {
   6311                normalizeSublists(items[i]);
   6312            }
   6313 
   6314            // "Record the values of the one-node list consisting of node, and
   6315            // let values be the result."
   6316            var values = recordValues([node]);
   6317 
   6318            // "Split the parent of the one-node list consisting of node."
   6319            splitParent([node]);
   6320 
   6321            // "Restore the values from values."
   6322            restoreValues(values);
   6323 
   6324            // "If node is a dd or dt, and it is not an allowed child of any of
   6325            // its ancestors in the same editing host, set the tag name of node
   6326            // to the default single-line container name and let node be the
   6327            // result."
   6328            if (isHtmlElement(node, ["dd", "dt"])
   6329            && getAncestors(node).every(function(ancestor) {
   6330                return !inSameEditingHost(node, ancestor)
   6331                    || !isAllowedChild(node, ancestor)
   6332            })) {
   6333                node = setTagName(node, defaultSingleLineContainerName);
   6334            }
   6335 
   6336            // "Fix disallowed ancestors of node."
   6337            fixDisallowedAncestors(node);
   6338 
   6339            // "Return true."
   6340            return true;
   6341        }
   6342 
   6343        // "Let start node equal node and let start offset equal offset."
   6344        var startNode = node;
   6345        var startOffset = offset;
   6346 
   6347        // "Repeat the following steps:"
   6348        while (true) {
   6349            // "If start offset is zero, set start offset to the index of start
   6350            // node and then set start node to its parent."
   6351            if (startOffset == 0) {
   6352                startOffset = getNodeIndex(startNode);
   6353                startNode = startNode.parentNode;
   6354 
   6355            // "Otherwise, if start node has an editable invisible child with
   6356            // index start offset minus one, remove it from start node and
   6357            // subtract one from start offset."
   6358            } else if (0 <= startOffset - 1
   6359            && startOffset - 1 < startNode.childNodes.length
   6360            && isEditable(startNode.childNodes[startOffset - 1])
   6361            && isInvisible(startNode.childNodes[startOffset - 1])) {
   6362                startNode.removeChild(startNode.childNodes[startOffset - 1]);
   6363                startOffset--;
   6364 
   6365            // "Otherwise, break from this loop."
   6366            } else {
   6367                break;
   6368            }
   6369        }
   6370 
   6371        // "If offset is zero, and node has an editable ancestor container in
   6372        // the same editing host that's an indentation element:"
   6373        if (offset == 0
   6374        && getAncestors(node).concat(node).filter(function(ancestor) {
   6375            return isEditable(ancestor)
   6376                && inSameEditingHost(ancestor, node)
   6377                && isIndentationElement(ancestor);
   6378        }).length) {
   6379            // "Block-extend the range whose start and end are both (node, 0),
   6380            // and let new range be the result."
   6381            var newRange = document.createRange();
   6382            newRange.setStart(node, 0);
   6383            newRange = blockExtend(newRange);
   6384 
   6385            // "Let node list be a list of nodes, initially empty."
   6386            //
   6387            // "For each node current node contained in new range, append
   6388            // current node to node list if the last member of node list (if
   6389            // any) is not an ancestor of current node, and current node is
   6390            // editable but has no editable descendants."
   6391            var nodeList = getContainedNodes(newRange, function(currentNode) {
   6392                return isEditable(currentNode)
   6393                    && !hasEditableDescendants(currentNode);
   6394            });
   6395 
   6396            // "Outdent each node in node list."
   6397            for (var i = 0; i < nodeList.length; i++) {
   6398                outdentNode(nodeList[i]);
   6399            }
   6400 
   6401            // "Return true."
   6402            return true;
   6403        }
   6404 
   6405        // "If the child of start node with index start offset is a table,
   6406        // return true."
   6407        if (isHtmlElement(startNode.childNodes[startOffset], "table")) {
   6408            return true;
   6409        }
   6410 
   6411        // "If start node has a child with index start offset − 1, and that
   6412        // child is a table:"
   6413        if (0 <= startOffset - 1
   6414        && startOffset - 1 < startNode.childNodes.length
   6415        && isHtmlElement(startNode.childNodes[startOffset - 1], "table")) {
   6416            // "Call collapse(start node, start offset − 1) on the context
   6417            // object's Selection."
   6418            getSelection().collapse(startNode, startOffset - 1);
   6419            getActiveRange().setStart(startNode, startOffset - 1);
   6420 
   6421            // "Call extend(start node, start offset) on the context object's
   6422            // Selection."
   6423            getSelection().extend(startNode, startOffset);
   6424            getActiveRange().setEnd(startNode, startOffset);
   6425 
   6426            // "Return true."
   6427            return true;
   6428        }
   6429 
   6430        // "If offset is zero; and either the child of start node with index
   6431        // start offset minus one is an hr, or the child is a br whose
   6432        // previousSibling is either a br or not an inline node:"
   6433        if (offset == 0
   6434        && (isHtmlElement(startNode.childNodes[startOffset - 1], "hr")
   6435            || (
   6436                isHtmlElement(startNode.childNodes[startOffset - 1], "br")
   6437                && (
   6438                    isHtmlElement(startNode.childNodes[startOffset - 1].previousSibling, "br")
   6439                    || !isInlineNode(startNode.childNodes[startOffset - 1].previousSibling)
   6440                )
   6441            )
   6442        )) {
   6443            // "Call collapse(start node, start offset − 1) on the context
   6444            // object's Selection."
   6445            getSelection().collapse(startNode, startOffset - 1);
   6446            getActiveRange().setStart(startNode, startOffset - 1);
   6447 
   6448            // "Call extend(start node, start offset) on the context object's
   6449            // Selection."
   6450            getSelection().extend(startNode, startOffset);
   6451            getActiveRange().setEnd(startNode, startOffset);
   6452 
   6453            // "Delete the selection."
   6454            deleteSelection();
   6455 
   6456            // "Call collapse(node, offset) on the Selection."
   6457            getSelection().collapse(node, offset);
   6458            getActiveRange().setStart(node, offset);
   6459            getActiveRange().collapse(true);
   6460 
   6461            // "Return true."
   6462            return true;
   6463        }
   6464 
   6465        // "If the child of start node with index start offset is an li or dt
   6466        // or dd, and that child's firstChild is an inline node, and start
   6467        // offset is not zero:"
   6468        if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"])
   6469        && isInlineNode(startNode.childNodes[startOffset].firstChild)
   6470        && startOffset != 0) {
   6471            // "Let previous item be the child of start node with index start
   6472            // offset minus one."
   6473            var previousItem = startNode.childNodes[startOffset - 1];
   6474 
   6475            // "If previous item's lastChild is an inline node other than a br,
   6476            // call createElement("br") on the context object and append the
   6477            // result as the last child of previous item."
   6478            if (isInlineNode(previousItem.lastChild)
   6479            && !isHtmlElement(previousItem.lastChild, "br")) {
   6480                previousItem.appendChild(document.createElement("br"));
   6481            }
   6482 
   6483            // "If previous item's lastChild is an inline node, call
   6484            // createElement("br") on the context object and append the result
   6485            // as the last child of previous item."
   6486            if (isInlineNode(previousItem.lastChild)) {
   6487                previousItem.appendChild(document.createElement("br"));
   6488            }
   6489        }
   6490 
   6491        // "If start node's child with index start offset is an li or dt or dd,
   6492        // and that child's previousSibling is also an li or dt or dd:"
   6493        if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"])
   6494        && isHtmlElement(startNode.childNodes[startOffset].previousSibling, ["li", "dt", "dd"])) {
   6495            // "Call cloneRange() on the active range, and let original range
   6496            // be the result."
   6497            //
   6498            // We need to add it to extraRanges so it will actually get updated
   6499            // when moving preserving ranges.
   6500            var originalRange = getActiveRange().cloneRange();
   6501            extraRanges.push(originalRange);
   6502 
   6503            // "Set start node to its child with index start offset − 1."
   6504            startNode = startNode.childNodes[startOffset - 1];
   6505 
   6506            // "Set start offset to start node's length."
   6507            startOffset = getNodeLength(startNode);
   6508 
   6509            // "Set node to start node's nextSibling."
   6510            node = startNode.nextSibling;
   6511 
   6512            // "Call collapse(start node, start offset) on the context object's
   6513            // Selection."
   6514            getSelection().collapse(startNode, startOffset);
   6515            getActiveRange().setStart(startNode, startOffset);
   6516 
   6517            // "Call extend(node, 0) on the context object's Selection."
   6518            getSelection().extend(node, 0);
   6519            getActiveRange().setEnd(node, 0);
   6520 
   6521            // "Delete the selection."
   6522            deleteSelection();
   6523 
   6524            // "Call removeAllRanges() on the context object's Selection."
   6525            getSelection().removeAllRanges();
   6526 
   6527            // "Call addRange(original range) on the context object's
   6528            // Selection."
   6529            getSelection().addRange(originalRange);
   6530            getActiveRange().setStart(originalRange.startContainer, originalRange.startOffset);
   6531            getActiveRange().setEnd(originalRange.endContainer, originalRange.endOffset);
   6532 
   6533            // "Return true."
   6534            extraRanges.pop();
   6535            return true;
   6536        }
   6537 
   6538        // "While start node has a child with index start offset minus one:"
   6539        while (0 <= startOffset - 1
   6540        && startOffset - 1 < startNode.childNodes.length) {
   6541            // "If start node's child with index start offset minus one is
   6542            // editable and invisible, remove it from start node, then subtract
   6543            // one from start offset."
   6544            if (isEditable(startNode.childNodes[startOffset - 1])
   6545            && isInvisible(startNode.childNodes[startOffset - 1])) {
   6546                startNode.removeChild(startNode.childNodes[startOffset - 1]);
   6547                startOffset--;
   6548 
   6549            // "Otherwise, set start node to its child with index start offset
   6550            // minus one, then set start offset to the length of start node."
   6551            } else {
   6552                startNode = startNode.childNodes[startOffset - 1];
   6553                startOffset = getNodeLength(startNode);
   6554            }
   6555        }
   6556 
   6557        // "Call collapse(start node, start offset) on the context object's
   6558        // Selection."
   6559        getSelection().collapse(startNode, startOffset);
   6560        getActiveRange().setStart(startNode, startOffset);
   6561 
   6562        // "Call extend(node, offset) on the context object's Selection."
   6563        getSelection().extend(node, offset);
   6564        getActiveRange().setEnd(node, offset);
   6565 
   6566        // "Delete the selection, with direction "backward"."
   6567        deleteSelection({direction: "backward"});
   6568 
   6569        // "Return true."
   6570        return true;
   6571    }
   6572 };
   6573 
   6574 //@}
   6575 ///// The formatBlock command /////
   6576 //@{
   6577 // "A formattable block name is "address", "dd", "div", "dt", "h1", "h2", "h3",
   6578 // "h4", "h5", "h6", "p", or "pre"."
   6579 var formattableBlockNames = ["address", "dd", "div", "dt", "h1", "h2", "h3",
   6580    "h4", "h5", "h6", "p", "pre"];
   6581 
   6582 commands.formatblock = {
   6583    preservesOverrides: true,
   6584    action: function(value) {
   6585        // "If value begins with a "<" character and ends with a ">" character,
   6586        // remove the first and last characters from it."
   6587        if (/^<.*>$/.test(value)) {
   6588            value = value.slice(1, -1);
   6589        }
   6590 
   6591        // "Let value be converted to ASCII lowercase."
   6592        value = value.toLowerCase();
   6593 
   6594        // "If value is not a formattable block name, return false."
   6595        if (formattableBlockNames.indexOf(value) == -1) {
   6596            return false;
   6597        }
   6598 
   6599        // "Block-extend the active range, and let new range be the result."
   6600        var newRange = blockExtend(getActiveRange());
   6601 
   6602        // "Let node list be an empty list of nodes."
   6603        //
   6604        // "For each node node contained in new range, append node to node list
   6605        // if it is editable, the last member of original node list (if any) is
   6606        // not an ancestor of node, node is either a non-list single-line
   6607        // container or an allowed child of "p" or a dd or dt, and node is not
   6608        // the ancestor of a prohibited paragraph child."
   6609        var nodeList = getContainedNodes(newRange, function(node) {
   6610            return isEditable(node)
   6611                && (isNonListSingleLineContainer(node)
   6612                || isAllowedChild(node, "p")
   6613                || isHtmlElement(node, ["dd", "dt"]))
   6614                && !getDescendants(node).some(isProhibitedParagraphChild);
   6615        });
   6616 
   6617        // "Record the values of node list, and let values be the result."
   6618        var values = recordValues(nodeList);
   6619 
   6620        // "For each node in node list, while node is the descendant of an
   6621        // editable HTML element in the same editing host, whose local name is
   6622        // a formattable block name, and which is not the ancestor of a
   6623        // prohibited paragraph child, split the parent of the one-node list
   6624        // consisting of node."
   6625        for (var i = 0; i < nodeList.length; i++) {
   6626            var node = nodeList[i];
   6627            while (getAncestors(node).some(function(ancestor) {
   6628                return isEditable(ancestor)
   6629                    && inSameEditingHost(ancestor, node)
   6630                    && isHtmlElement(ancestor, formattableBlockNames)
   6631                    && !getDescendants(ancestor).some(isProhibitedParagraphChild);
   6632            })) {
   6633                splitParent([node]);
   6634            }
   6635        }
   6636 
   6637        // "Restore the values from values."
   6638        restoreValues(values);
   6639 
   6640        // "While node list is not empty:"
   6641        while (nodeList.length) {
   6642            var sublist;
   6643 
   6644            // "If the first member of node list is a single-line
   6645            // container:"
   6646            if (isSingleLineContainer(nodeList[0])) {
   6647                // "Let sublist be the children of the first member of node
   6648                // list."
   6649                sublist = [].slice.call(nodeList[0].childNodes);
   6650 
   6651                // "Record the values of sublist, and let values be the
   6652                // result."
   6653                var values = recordValues(sublist);
   6654 
   6655                // "Remove the first member of node list from its parent,
   6656                // preserving its descendants."
   6657                removePreservingDescendants(nodeList[0]);
   6658 
   6659                // "Restore the values from values."
   6660                restoreValues(values);
   6661 
   6662                // "Remove the first member from node list."
   6663                nodeList.shift();
   6664 
   6665            // "Otherwise:"
   6666            } else {
   6667                // "Let sublist be an empty list of nodes."
   6668                sublist = [];
   6669 
   6670                // "Remove the first member of node list and append it to
   6671                // sublist."
   6672                sublist.push(nodeList.shift());
   6673 
   6674                // "While node list is not empty, and the first member of
   6675                // node list is the nextSibling of the last member of
   6676                // sublist, and the first member of node list is not a
   6677                // single-line container, and the last member of sublist is
   6678                // not a br, remove the first member of node list and
   6679                // append it to sublist."
   6680                while (nodeList.length
   6681                && nodeList[0] == sublist[sublist.length - 1].nextSibling
   6682                && !isSingleLineContainer(nodeList[0])
   6683                && !isHtmlElement(sublist[sublist.length - 1], "BR")) {
   6684                    sublist.push(nodeList.shift());
   6685                }
   6686            }
   6687 
   6688            // "Wrap sublist. If value is "div" or "p", sibling criteria
   6689            // returns false; otherwise it returns true for an HTML element
   6690            // with local name value and no attributes, and false otherwise.
   6691            // New parent instructions return the result of running
   6692            // createElement(value) on the context object. Then fix disallowed
   6693            // ancestors of the result."
   6694            fixDisallowedAncestors(wrap(sublist,
   6695                ["div", "p"].indexOf(value) == - 1
   6696                    ? function(node) { return isHtmlElement(node, value) && !node.attributes.length }
   6697                    : function() { return false },
   6698                function() { return document.createElement(value) }));
   6699        }
   6700 
   6701        // "Return true."
   6702        return true;
   6703    }, indeterm: function() {
   6704        // "If the active range is null, return false."
   6705        if (!getActiveRange()) {
   6706            return false;
   6707        }
   6708 
   6709        // "Block-extend the active range, and let new range be the result."
   6710        var newRange = blockExtend(getActiveRange());
   6711 
   6712        // "Let node list be all visible editable nodes that are contained in
   6713        // new range and have no children."
   6714        var nodeList = getAllContainedNodes(newRange, function(node) {
   6715            return isVisible(node)
   6716                && isEditable(node)
   6717                && !node.hasChildNodes();
   6718        });
   6719 
   6720        // "If node list is empty, return false."
   6721        if (!nodeList.length) {
   6722            return false;
   6723        }
   6724 
   6725        // "Let type be null."
   6726        var type = null;
   6727 
   6728        // "For each node in node list:"
   6729        for (var i = 0; i < nodeList.length; i++) {
   6730            var node = nodeList[i];
   6731 
   6732            // "While node's parent is editable and in the same editing host as
   6733            // node, and node is not an HTML element whose local name is a
   6734            // formattable block name, set node to its parent."
   6735            while (isEditable(node.parentNode)
   6736            && inSameEditingHost(node, node.parentNode)
   6737            && !isHtmlElement(node, formattableBlockNames)) {
   6738                node = node.parentNode;
   6739            }
   6740 
   6741            // "Let current type be the empty string."
   6742            var currentType = "";
   6743 
   6744            // "If node is an editable HTML element whose local name is a
   6745            // formattable block name, and node is not the ancestor of a
   6746            // prohibited paragraph child, set current type to node's local
   6747            // name."
   6748            if (isEditable(node)
   6749            && isHtmlElement(node, formattableBlockNames)
   6750            && !getDescendants(node).some(isProhibitedParagraphChild)) {
   6751                currentType = node.tagName;
   6752            }
   6753 
   6754            // "If type is null, set type to current type."
   6755            if (type === null) {
   6756                type = currentType;
   6757 
   6758            // "Otherwise, if type does not equal current type, return true."
   6759            } else if (type != currentType) {
   6760                return true;
   6761            }
   6762        }
   6763 
   6764        // "Return false."
   6765        return false;
   6766    }, value: function() {
   6767        // "If the active range is null, return the empty string."
   6768        if (!getActiveRange()) {
   6769            return "";
   6770        }
   6771 
   6772        // "Block-extend the active range, and let new range be the result."
   6773        var newRange = blockExtend(getActiveRange());
   6774 
   6775        // "Let node be the first visible editable node that is contained in
   6776        // new range and has no children. If there is no such node, return the
   6777        // empty string."
   6778        var nodes = getAllContainedNodes(newRange, function(node) {
   6779            return isVisible(node)
   6780                && isEditable(node)
   6781                && !node.hasChildNodes();
   6782        });
   6783        if (!nodes.length) {
   6784            return "";
   6785        }
   6786        var node = nodes[0];
   6787 
   6788        // "While node's parent is editable and in the same editing host as
   6789        // node, and node is not an HTML element whose local name is a
   6790        // formattable block name, set node to its parent."
   6791        while (isEditable(node.parentNode)
   6792        && inSameEditingHost(node, node.parentNode)
   6793        && !isHtmlElement(node, formattableBlockNames)) {
   6794            node = node.parentNode;
   6795        }
   6796 
   6797        // "If node is an editable HTML element whose local name is a
   6798        // formattable block name, and node is not the ancestor of a prohibited
   6799        // paragraph child, return node's local name, converted to ASCII
   6800        // lowercase."
   6801        if (isEditable(node)
   6802        && isHtmlElement(node, formattableBlockNames)
   6803        && !getDescendants(node).some(isProhibitedParagraphChild)) {
   6804            return node.tagName.toLowerCase();
   6805        }
   6806 
   6807        // "Return the empty string."
   6808        return "";
   6809    }
   6810 };
   6811 
   6812 //@}
   6813 ///// The forwardDelete command /////
   6814 //@{
   6815 commands.forwarddelete = {
   6816    preservesOverrides: true,
   6817    action: function() {
   6818        // "If the active range is not collapsed, delete the selection and
   6819        // return true."
   6820        if (!getActiveRange().collapsed) {
   6821            deleteSelection();
   6822            return true;
   6823        }
   6824 
   6825        // "Canonicalize whitespace at the active range's start."
   6826        canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset);
   6827 
   6828        // "Let node and offset be the active range's start node and offset."
   6829        var node = getActiveRange().startContainer;
   6830        var offset = getActiveRange().startOffset;
   6831 
   6832        // "Repeat the following steps:"
   6833        while (true) {
   6834            // "If offset is the length of node and node's nextSibling is an
   6835            // editable invisible node, remove node's nextSibling from its
   6836            // parent."
   6837            if (offset == getNodeLength(node)
   6838            && isEditable(node.nextSibling)
   6839            && isInvisible(node.nextSibling)) {
   6840                node.parentNode.removeChild(node.nextSibling);
   6841 
   6842            // "Otherwise, if node has a child with index offset and that child
   6843            // is an editable invisible node, remove that child from node."
   6844            } else if (offset < node.childNodes.length
   6845            && isEditable(node.childNodes[offset])
   6846            && isInvisible(node.childNodes[offset])) {
   6847                node.removeChild(node.childNodes[offset]);
   6848 
   6849            // "Otherwise, if offset is the length of node and node is an
   6850            // inline node, or if node is invisible, set offset to one plus the
   6851            // index of node, then set node to its parent."
   6852            } else if ((offset == getNodeLength(node)
   6853            && isInlineNode(node))
   6854            || isInvisible(node)) {
   6855                offset = 1 + getNodeIndex(node);
   6856                node = node.parentNode;
   6857 
   6858            // "Otherwise, if node has a child with index offset and that child
   6859            // is neither a block node nor a br nor an img nor a collapsed
   6860            // block prop, set node to that child, then set offset to zero."
   6861            } else if (offset < node.childNodes.length
   6862            && !isBlockNode(node.childNodes[offset])
   6863            && !isHtmlElement(node.childNodes[offset], ["br", "img"])
   6864            && !isCollapsedBlockProp(node.childNodes[offset])) {
   6865                node = node.childNodes[offset];
   6866                offset = 0;
   6867 
   6868            // "Otherwise, break from this loop."
   6869            } else {
   6870                break;
   6871            }
   6872        }
   6873 
   6874        // "If node is a Text node and offset is not node's length:"
   6875        if (node.nodeType == Node.TEXT_NODE
   6876        && offset != getNodeLength(node)) {
   6877            // "Let end offset be offset plus one."
   6878            var endOffset = offset + 1;
   6879 
   6880            // "While end offset is not node's length and the end offsetth
   6881            // element of node's data has general category M when interpreted
   6882            // as a Unicode code point, add one to end offset."
   6883            //
   6884            // TODO: Not even going to try handling anything beyond the most
   6885            // basic combining marks, since I couldn't find a good list.  I
   6886            // special-case a few Hebrew diacritics too to test basic coverage
   6887            // of non-Latin stuff.
   6888            while (endOffset != node.length
   6889            && /^[\u0300-\u036f\u0591-\u05bd\u05c1\u05c2]$/.test(node.data[endOffset])) {
   6890                endOffset++;
   6891            }
   6892 
   6893            // "Call collapse(node, offset) on the context object's Selection."
   6894            getSelection().collapse(node, offset);
   6895            getActiveRange().setStart(node, offset);
   6896 
   6897            // "Call extend(node, end offset) on the context object's
   6898            // Selection."
   6899            getSelection().extend(node, endOffset);
   6900            getActiveRange().setEnd(node, endOffset);
   6901 
   6902            // "Delete the selection."
   6903            deleteSelection();
   6904 
   6905            // "Return true."
   6906            return true;
   6907        }
   6908 
   6909        // "If node is an inline node, return true."
   6910        if (isInlineNode(node)) {
   6911            return true;
   6912        }
   6913 
   6914        // "If node has a child with index offset and that child is a br or hr
   6915        // or img, but is not a collapsed block prop:"
   6916        if (offset < node.childNodes.length
   6917        && isHtmlElement(node.childNodes[offset], ["br", "hr", "img"])
   6918        && !isCollapsedBlockProp(node.childNodes[offset])) {
   6919            // "Call collapse(node, offset) on the context object's Selection."
   6920            getSelection().collapse(node, offset);
   6921            getActiveRange().setStart(node, offset);
   6922 
   6923            // "Call extend(node, offset + 1) on the context object's
   6924            // Selection."
   6925            getSelection().extend(node, offset + 1);
   6926            getActiveRange().setEnd(node, offset + 1);
   6927 
   6928            // "Delete the selection."
   6929            deleteSelection();
   6930 
   6931            // "Return true."
   6932            return true;
   6933        }
   6934 
   6935        // "Let end node equal node and let end offset equal offset."
   6936        var endNode = node;
   6937        var endOffset = offset;
   6938 
   6939        // "If end node has a child with index end offset, and that child is a
   6940        // collapsed block prop, add one to end offset."
   6941        if (endOffset < endNode.childNodes.length
   6942        && isCollapsedBlockProp(endNode.childNodes[endOffset])) {
   6943            endOffset++;
   6944        }
   6945 
   6946        // "Repeat the following steps:"
   6947        while (true) {
   6948            // "If end offset is the length of end node, set end offset to one
   6949            // plus the index of end node and then set end node to its parent."
   6950            if (endOffset == getNodeLength(endNode)) {
   6951                endOffset = 1 + getNodeIndex(endNode);
   6952                endNode = endNode.parentNode;
   6953 
   6954            // "Otherwise, if end node has a an editable invisible child with
   6955            // index end offset, remove it from end node."
   6956            } else if (endOffset < endNode.childNodes.length
   6957            && isEditable(endNode.childNodes[endOffset])
   6958            && isInvisible(endNode.childNodes[endOffset])) {
   6959                endNode.removeChild(endNode.childNodes[endOffset]);
   6960 
   6961            // "Otherwise, break from this loop."
   6962            } else {
   6963                break;
   6964            }
   6965        }
   6966 
   6967        // "If the child of end node with index end offset minus one is a
   6968        // table, return true."
   6969        if (isHtmlElement(endNode.childNodes[endOffset - 1], "table")) {
   6970            return true;
   6971        }
   6972 
   6973        // "If the child of end node with index end offset is a table:"
   6974        if (isHtmlElement(endNode.childNodes[endOffset], "table")) {
   6975            // "Call collapse(end node, end offset) on the context object's
   6976            // Selection."
   6977            getSelection().collapse(endNode, endOffset);
   6978            getActiveRange().setStart(endNode, endOffset);
   6979 
   6980            // "Call extend(end node, end offset + 1) on the context object's
   6981            // Selection."
   6982            getSelection().extend(endNode, endOffset + 1);
   6983            getActiveRange().setEnd(endNode, endOffset + 1);
   6984 
   6985            // "Return true."
   6986            return true;
   6987        }
   6988 
   6989        // "If offset is the length of node, and the child of end node with
   6990        // index end offset is an hr or br:"
   6991        if (offset == getNodeLength(node)
   6992        && isHtmlElement(endNode.childNodes[endOffset], ["br", "hr"])) {
   6993            // "Call collapse(end node, end offset) on the context object's
   6994            // Selection."
   6995            getSelection().collapse(endNode, endOffset);
   6996            getActiveRange().setStart(endNode, endOffset);
   6997 
   6998            // "Call extend(end node, end offset + 1) on the context object's
   6999            // Selection."
   7000            getSelection().extend(endNode, endOffset + 1);
   7001            getActiveRange().setEnd(endNode, endOffset + 1);
   7002 
   7003            // "Delete the selection."
   7004            deleteSelection();
   7005 
   7006            // "Call collapse(node, offset) on the Selection."
   7007            getSelection().collapse(node, offset);
   7008            getActiveRange().setStart(node, offset);
   7009            getActiveRange().collapse(true);
   7010 
   7011            // "Return true."
   7012            return true;
   7013        }
   7014 
   7015        // "While end node has a child with index end offset:"
   7016        while (endOffset < endNode.childNodes.length) {
   7017            // "If end node's child with index end offset is editable and
   7018            // invisible, remove it from end node."
   7019            if (isEditable(endNode.childNodes[endOffset])
   7020            && isInvisible(endNode.childNodes[endOffset])) {
   7021                endNode.removeChild(endNode.childNodes[endOffset]);
   7022 
   7023            // "Otherwise, set end node to its child with index end offset and
   7024            // set end offset to zero."
   7025            } else {
   7026                endNode = endNode.childNodes[endOffset];
   7027                endOffset = 0;
   7028            }
   7029        }
   7030 
   7031        // "Call collapse(node, offset) on the context object's Selection."
   7032        getSelection().collapse(node, offset);
   7033        getActiveRange().setStart(node, offset);
   7034 
   7035        // "Call extend(end node, end offset) on the context object's
   7036        // Selection."
   7037        getSelection().extend(endNode, endOffset);
   7038        getActiveRange().setEnd(endNode, endOffset);
   7039 
   7040        // "Delete the selection."
   7041        deleteSelection();
   7042 
   7043        // "Return true."
   7044        return true;
   7045    }
   7046 };
   7047 
   7048 //@}
   7049 ///// The indent command /////
   7050 //@{
   7051 commands.indent = {
   7052    preservesOverrides: true,
   7053    action: function() {
   7054        // "Let items be a list of all lis that are ancestor containers of the
   7055        // active range's start and/or end node."
   7056        //
   7057        // Has to be in tree order, remember!
   7058        var items = [];
   7059        for (var node = getActiveRange().endContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) {
   7060            if (isHtmlElement(node, "LI")) {
   7061                items.unshift(node);
   7062            }
   7063        }
   7064        for (var node = getActiveRange().startContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) {
   7065            if (isHtmlElement(node, "LI")) {
   7066                items.unshift(node);
   7067            }
   7068        }
   7069        for (var node = getActiveRange().commonAncestorContainer; node; node = node.parentNode) {
   7070            if (isHtmlElement(node, "LI")) {
   7071                items.unshift(node);
   7072            }
   7073        }
   7074 
   7075        // "For each item in items, normalize sublists of item."
   7076        for (var i = 0; i < items.length; i++) {
   7077            normalizeSublists(items[i]);
   7078        }
   7079 
   7080        // "Block-extend the active range, and let new range be the result."
   7081        var newRange = blockExtend(getActiveRange());
   7082 
   7083        // "Let node list be a list of nodes, initially empty."
   7084        var nodeList = [];
   7085 
   7086        // "For each node node contained in new range, if node is editable and
   7087        // is an allowed child of "div" or "ol" and if the last member of node
   7088        // list (if any) is not an ancestor of node, append node to node list."
   7089        nodeList = getContainedNodes(newRange, function(node) {
   7090            return isEditable(node)
   7091                && (isAllowedChild(node, "div")
   7092                || isAllowedChild(node, "ol"));
   7093        });
   7094 
   7095        // "If the first visible member of node list is an li whose parent is
   7096        // an ol or ul:"
   7097        if (isHtmlElement(nodeList.filter(isVisible)[0], "li")
   7098        && isHtmlElement(nodeList.filter(isVisible)[0].parentNode, ["ol", "ul"])) {
   7099            // "Let sibling be node list's first visible member's
   7100            // previousSibling."
   7101            var sibling = nodeList.filter(isVisible)[0].previousSibling;
   7102 
   7103            // "While sibling is invisible, set sibling to its
   7104            // previousSibling."
   7105            while (isInvisible(sibling)) {
   7106                sibling = sibling.previousSibling;
   7107            }
   7108 
   7109            // "If sibling is an li, normalize sublists of sibling."
   7110            if (isHtmlElement(sibling, "li")) {
   7111                normalizeSublists(sibling);
   7112            }
   7113        }
   7114 
   7115        // "While node list is not empty:"
   7116        while (nodeList.length) {
   7117            // "Let sublist be a list of nodes, initially empty."
   7118            var sublist = [];
   7119 
   7120            // "Remove the first member of node list and append it to sublist."
   7121            sublist.push(nodeList.shift());
   7122 
   7123            // "While the first member of node list is the nextSibling of the
   7124            // last member of sublist, remove the first member of node list and
   7125            // append it to sublist."
   7126            while (nodeList.length
   7127            && nodeList[0] == sublist[sublist.length - 1].nextSibling) {
   7128                sublist.push(nodeList.shift());
   7129            }
   7130 
   7131            // "Indent sublist."
   7132            indentNodes(sublist);
   7133        }
   7134 
   7135        // "Return true."
   7136        return true;
   7137    }
   7138 };
   7139 
   7140 //@}
   7141 ///// The insertHorizontalRule command /////
   7142 //@{
   7143 commands.inserthorizontalrule = {
   7144    preservesOverrides: true,
   7145    action: function() {
   7146        // "Let start node, start offset, end node, and end offset be the
   7147        // active range's start and end nodes and offsets."
   7148        var startNode = getActiveRange().startContainer;
   7149        var startOffset = getActiveRange().startOffset;
   7150        var endNode = getActiveRange().endContainer;
   7151        var endOffset = getActiveRange().endOffset;
   7152 
   7153        // "While start offset is 0 and start node's parent is not null, set
   7154        // start offset to start node's index, then set start node to its
   7155        // parent."
   7156        while (startOffset == 0
   7157        && startNode.parentNode) {
   7158            startOffset = getNodeIndex(startNode);
   7159            startNode = startNode.parentNode;
   7160        }
   7161 
   7162        // "While end offset is end node's length, and end node's parent is not
   7163        // null, set end offset to one plus end node's index, then set end node
   7164        // to its parent."
   7165        while (endOffset == getNodeLength(endNode)
   7166        && endNode.parentNode) {
   7167            endOffset = 1 + getNodeIndex(endNode);
   7168            endNode = endNode.parentNode;
   7169        }
   7170 
   7171        // "Call collapse(start node, start offset) on the context object's
   7172        // Selection."
   7173        getSelection().collapse(startNode, startOffset);
   7174        getActiveRange().setStart(startNode, startOffset);
   7175 
   7176        // "Call extend(end node, end offset) on the context object's
   7177        // Selection."
   7178        getSelection().extend(endNode, endOffset);
   7179        getActiveRange().setEnd(endNode, endOffset);
   7180 
   7181        // "Delete the selection, with block merging false."
   7182        deleteSelection({blockMerging: false});
   7183 
   7184        // "If the active range's start node is neither editable nor an editing
   7185        // host, return true."
   7186        if (!isEditable(getActiveRange().startContainer)
   7187        && !isEditingHost(getActiveRange().startContainer)) {
   7188            return true;
   7189        }
   7190 
   7191        // "If the active range's start node is a Text node and its start
   7192        // offset is zero, call collapse() on the context object's Selection,
   7193        // with first argument the active range's start node's parent and
   7194        // second argument the active range's start node's index."
   7195        if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
   7196        && getActiveRange().startOffset == 0) {
   7197            var newNode = getActiveRange().startContainer.parentNode;
   7198            var newOffset = getNodeIndex(getActiveRange().startContainer);
   7199            getSelection().collapse(newNode, newOffset);
   7200            getActiveRange().setStart(newNode, newOffset);
   7201            getActiveRange().collapse(true);
   7202        }
   7203 
   7204        // "If the active range's start node is a Text node and its start
   7205        // offset is the length of its start node, call collapse() on the
   7206        // context object's Selection, with first argument the active range's
   7207        // start node's parent, and the second argument one plus the active
   7208        // range's start node's index."
   7209        if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
   7210        && getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) {
   7211            var newNode = getActiveRange().startContainer.parentNode;
   7212            var newOffset = 1 + getNodeIndex(getActiveRange().startContainer);
   7213            getSelection().collapse(newNode, newOffset);
   7214            getActiveRange().setStart(newNode, newOffset);
   7215            getActiveRange().collapse(true);
   7216        }
   7217 
   7218        // "Let hr be the result of calling createElement("hr") on the
   7219        // context object."
   7220        var hr = document.createElement("hr");
   7221 
   7222        // "Run insertNode(hr) on the active range."
   7223        getActiveRange().insertNode(hr);
   7224 
   7225        // "Fix disallowed ancestors of hr."
   7226        fixDisallowedAncestors(hr);
   7227 
   7228        // "Run collapse() on the context object's Selection, with first
   7229        // argument hr's parent and the second argument equal to one plus hr's
   7230        // index."
   7231        getSelection().collapse(hr.parentNode, 1 + getNodeIndex(hr));
   7232        getActiveRange().setStart(hr.parentNode, 1 + getNodeIndex(hr));
   7233        getActiveRange().collapse(true);
   7234 
   7235        // "Return true."
   7236        return true;
   7237    }
   7238 };
   7239 
   7240 //@}
   7241 ///// The insertHTML command /////
   7242 //@{
   7243 commands.inserthtml = {
   7244    preservesOverrides: true,
   7245    action: function(value) {
   7246        // "Delete the selection."
   7247        deleteSelection();
   7248 
   7249        // "If the active range's start node is neither editable nor an editing
   7250        // host, return true."
   7251        if (!isEditable(getActiveRange().startContainer)
   7252        && !isEditingHost(getActiveRange().startContainer)) {
   7253            return true;
   7254        }
   7255 
   7256        // "Let frag be the result of calling createContextualFragment(value)
   7257        // on the active range."
   7258        var frag = getActiveRange().createContextualFragment(value);
   7259 
   7260        // "Let last child be the lastChild of frag."
   7261        var lastChild = frag.lastChild;
   7262 
   7263        // "If last child is null, return true."
   7264        if (!lastChild) {
   7265            return true;
   7266        }
   7267 
   7268        // "Let descendants be all descendants of frag."
   7269        var descendants = getDescendants(frag);
   7270 
   7271        // "If the active range's start node is a block node:"
   7272        if (isBlockNode(getActiveRange().startContainer)) {
   7273            // "Let collapsed block props be all editable collapsed block prop
   7274            // children of the active range's start node that have index
   7275            // greater than or equal to the active range's start offset."
   7276            //
   7277            // "For each node in collapsed block props, remove node from its
   7278            // parent."
   7279            [].filter.call(getActiveRange().startContainer.childNodes, function(node) {
   7280                return isEditable(node)
   7281                    && isCollapsedBlockProp(node)
   7282                    && getNodeIndex(node) >= getActiveRange().startOffset;
   7283            }).forEach(function(node) {
   7284                node.parentNode.removeChild(node);
   7285            });
   7286        }
   7287 
   7288        // "Call insertNode(frag) on the active range."
   7289        getActiveRange().insertNode(frag);
   7290 
   7291        // "If the active range's start node is a block node with no visible
   7292        // children, call createElement("br") on the context object and append
   7293        // the result as the last child of the active range's start node."
   7294        if (isBlockNode(getActiveRange().startContainer)
   7295        && ![].some.call(getActiveRange().startContainer.childNodes, isVisible)) {
   7296            getActiveRange().startContainer.appendChild(document.createElement("br"));
   7297        }
   7298 
   7299        // "Call collapse() on the context object's Selection, with last
   7300        // child's parent as the first argument and one plus its index as the
   7301        // second."
   7302        getActiveRange().setStart(lastChild.parentNode, 1 + getNodeIndex(lastChild));
   7303        getActiveRange().setEnd(lastChild.parentNode, 1 + getNodeIndex(lastChild));
   7304 
   7305        // "Fix disallowed ancestors of each member of descendants."
   7306        for (var i = 0; i < descendants.length; i++) {
   7307            fixDisallowedAncestors(descendants[i]);
   7308        }
   7309 
   7310        // "Return true."
   7311        return true;
   7312    }
   7313 };
   7314 
   7315 //@}
   7316 ///// The insertImage command /////
   7317 //@{
   7318 commands.insertimage = {
   7319    preservesOverrides: true,
   7320    action: function(value) {
   7321        // "If value is the empty string, return false."
   7322        if (value === "") {
   7323            return false;
   7324        }
   7325 
   7326        // "Delete the selection, with strip wrappers false."
   7327        deleteSelection({stripWrappers: false});
   7328 
   7329        // "Let range be the active range."
   7330        var range = getActiveRange();
   7331 
   7332        // "If the active range's start node is neither editable nor an editing
   7333        // host, return true."
   7334        if (!isEditable(getActiveRange().startContainer)
   7335        && !isEditingHost(getActiveRange().startContainer)) {
   7336            return true;
   7337        }
   7338 
   7339        // "If range's start node is a block node whose sole child is a br, and
   7340        // its start offset is 0, remove its start node's child from it."
   7341        if (isBlockNode(range.startContainer)
   7342        && range.startContainer.childNodes.length == 1
   7343        && isHtmlElement(range.startContainer.firstChild, "br")
   7344        && range.startOffset == 0) {
   7345            range.startContainer.removeChild(range.startContainer.firstChild);
   7346        }
   7347 
   7348        // "Let img be the result of calling createElement("img") on the
   7349        // context object."
   7350        var img = document.createElement("img");
   7351 
   7352        // "Run setAttribute("src", value) on img."
   7353        img.setAttribute("src", value);
   7354 
   7355        // "Run insertNode(img) on the range."
   7356        range.insertNode(img);
   7357 
   7358        // "Run collapse() on the Selection, with first argument equal to the
   7359        // parent of img and the second argument equal to one plus the index of
   7360        // img."
   7361        //
   7362        // Not everyone actually supports collapse(), so we do it manually
   7363        // instead.  Also, we need to modify the actual range we're given as
   7364        // well, for the sake of autoimplementation.html's range-filling-in.
   7365        range.setStart(img.parentNode, 1 + getNodeIndex(img));
   7366        range.setEnd(img.parentNode, 1 + getNodeIndex(img));
   7367        getSelection().removeAllRanges();
   7368        getSelection().addRange(range);
   7369 
   7370        // IE adds width and height attributes for some reason, so remove those
   7371        // to actually do what the spec says.
   7372        img.removeAttribute("width");
   7373        img.removeAttribute("height");
   7374 
   7375        // "Return true."
   7376        return true;
   7377    }
   7378 };
   7379 
   7380 //@}
   7381 ///// The insertLineBreak command /////
   7382 //@{
   7383 commands.insertlinebreak = {
   7384    preservesOverrides: true,
   7385    action: function(value) {
   7386        // "Delete the selection, with strip wrappers false."
   7387        deleteSelection({stripWrappers: false});
   7388 
   7389        // "If the active range's start node is neither editable nor an editing
   7390        // host, return true."
   7391        if (!isEditable(getActiveRange().startContainer)
   7392        && !isEditingHost(getActiveRange().startContainer)) {
   7393            return true;
   7394        }
   7395 
   7396        // "If the active range's start node is an Element, and "br" is not an
   7397        // allowed child of it, return true."
   7398        if (getActiveRange().startContainer.nodeType == Node.ELEMENT_NODE
   7399        && !isAllowedChild("br", getActiveRange().startContainer)) {
   7400            return true;
   7401        }
   7402 
   7403        // "If the active range's start node is not an Element, and "br" is not
   7404        // an allowed child of the active range's start node's parent, return
   7405        // true."
   7406        if (getActiveRange().startContainer.nodeType != Node.ELEMENT_NODE
   7407        && !isAllowedChild("br", getActiveRange().startContainer.parentNode)) {
   7408            return true;
   7409        }
   7410 
   7411        // "If the active range's start node is a Text node and its start
   7412        // offset is zero, call collapse() on the context object's Selection,
   7413        // with first argument equal to the active range's start node's parent
   7414        // and second argument equal to the active range's start node's index."
   7415        if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
   7416        && getActiveRange().startOffset == 0) {
   7417            var newNode = getActiveRange().startContainer.parentNode;
   7418            var newOffset = getNodeIndex(getActiveRange().startContainer);
   7419            getSelection().collapse(newNode, newOffset);
   7420            getActiveRange().setStart(newNode, newOffset);
   7421            getActiveRange().setEnd(newNode, newOffset);
   7422        }
   7423 
   7424        // "If the active range's start node is a Text node and its start
   7425        // offset is the length of its start node, call collapse() on the
   7426        // context object's Selection, with first argument equal to the active
   7427        // range's start node's parent and second argument equal to one plus
   7428        // the active range's start node's index."
   7429        if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
   7430        && getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) {
   7431            var newNode = getActiveRange().startContainer.parentNode;
   7432            var newOffset = 1 + getNodeIndex(getActiveRange().startContainer);
   7433            getSelection().collapse(newNode, newOffset);
   7434            getActiveRange().setStart(newNode, newOffset);
   7435            getActiveRange().setEnd(newNode, newOffset);
   7436        }
   7437 
   7438        // "Let br be the result of calling createElement("br") on the context
   7439        // object."
   7440        var br = document.createElement("br");
   7441 
   7442        // "Call insertNode(br) on the active range."
   7443        getActiveRange().insertNode(br);
   7444 
   7445        // "Call collapse() on the context object's Selection, with br's parent
   7446        // as the first argument and one plus br's index as the second
   7447        // argument."
   7448        getSelection().collapse(br.parentNode, 1 + getNodeIndex(br));
   7449        getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br));
   7450        getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br));
   7451 
   7452        // "If br is a collapsed line break, call createElement("br") on the
   7453        // context object and let extra br be the result, then call
   7454        // insertNode(extra br) on the active range."
   7455        if (isCollapsedLineBreak(br)) {
   7456            getActiveRange().insertNode(document.createElement("br"));
   7457 
   7458            // Compensate for nonstandard implementations of insertNode
   7459            getSelection().collapse(br.parentNode, 1 + getNodeIndex(br));
   7460            getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br));
   7461            getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br));
   7462        }
   7463 
   7464        // "Return true."
   7465        return true;
   7466    }
   7467 };
   7468 
   7469 //@}
   7470 ///// The insertOrderedList command /////
   7471 //@{
   7472 commands.insertorderedlist = {
   7473    preservesOverrides: true,
   7474    // "Toggle lists with tag name "ol", then return true."
   7475    action: function() { toggleLists("ol"); return true },
   7476    // "True if the selection's list state is "mixed" or "mixed ol", false
   7477    // otherwise."
   7478    indeterm: function() { return /^mixed( ol)?$/.test(getSelectionListState()) },
   7479    // "True if the selection's list state is "ol", false otherwise."
   7480    state: function() { return getSelectionListState() == "ol" },
   7481 };
   7482 
   7483 //@}
   7484 ///// The insertParagraph command /////
   7485 //@{
   7486 commands.insertparagraph = {
   7487    preservesOverrides: true,
   7488    action: function() {
   7489        // "Delete the selection."
   7490        deleteSelection();
   7491 
   7492        // "If the active range's start node is neither editable nor an editing
   7493        // host, return true."
   7494        if (!isEditable(getActiveRange().startContainer)
   7495        && !isEditingHost(getActiveRange().startContainer)) {
   7496            return true;
   7497        }
   7498 
   7499        // "Let node and offset be the active range's start node and offset."
   7500        var node = getActiveRange().startContainer;
   7501        var offset = getActiveRange().startOffset;
   7502 
   7503        // "If node is a Text node, and offset is neither 0 nor the length of
   7504        // node, call splitText(offset) on node."
   7505        if (node.nodeType == Node.TEXT_NODE
   7506        && offset != 0
   7507        && offset != getNodeLength(node)) {
   7508            node.splitText(offset);
   7509        }
   7510 
   7511        // "If node is a Text node and offset is its length, set offset to one
   7512        // plus the index of node, then set node to its parent."
   7513        if (node.nodeType == Node.TEXT_NODE
   7514        && offset == getNodeLength(node)) {
   7515            offset = 1 + getNodeIndex(node);
   7516            node = node.parentNode;
   7517        }
   7518 
   7519        // "If node is a Text or Comment node, set offset to the index of node,
   7520        // then set node to its parent."
   7521        if (node.nodeType == Node.TEXT_NODE
   7522        || node.nodeType == Node.COMMENT_NODE) {
   7523            offset = getNodeIndex(node);
   7524            node = node.parentNode;
   7525        }
   7526 
   7527        // "Call collapse(node, offset) on the context object's Selection."
   7528        getSelection().collapse(node, offset);
   7529        getActiveRange().setStart(node, offset);
   7530        getActiveRange().setEnd(node, offset);
   7531 
   7532        // "Let container equal node."
   7533        var container = node;
   7534 
   7535        // "While container is not a single-line container, and container's
   7536        // parent is editable and in the same editing host as node, set
   7537        // container to its parent."
   7538        while (!isSingleLineContainer(container)
   7539        && isEditable(container.parentNode)
   7540        && inSameEditingHost(node, container.parentNode)) {
   7541            container = container.parentNode;
   7542        }
   7543 
   7544        // "If container is an editable single-line container in the same
   7545        // editing host as node, and its local name is "p" or "div":"
   7546        if (isEditable(container)
   7547        && isSingleLineContainer(container)
   7548        && inSameEditingHost(node, container.parentNode)
   7549        && (container.tagName == "P" || container.tagName == "DIV")) {
   7550            // "Let outer container equal container."
   7551            var outerContainer = container;
   7552 
   7553            // "While outer container is not a dd or dt or li, and outer
   7554            // container's parent is editable, set outer container to its
   7555            // parent."
   7556            while (!isHtmlElement(outerContainer, ["dd", "dt", "li"])
   7557            && isEditable(outerContainer.parentNode)) {
   7558                outerContainer = outerContainer.parentNode;
   7559            }
   7560 
   7561            // "If outer container is a dd or dt or li, set container to outer
   7562            // container."
   7563            if (isHtmlElement(outerContainer, ["dd", "dt", "li"])) {
   7564                container = outerContainer;
   7565            }
   7566        }
   7567 
   7568        // "If container is not editable or not in the same editing host as
   7569        // node or is not a single-line container:"
   7570        if (!isEditable(container)
   7571        || !inSameEditingHost(container, node)
   7572        || !isSingleLineContainer(container)) {
   7573            // "Let tag be the default single-line container name."
   7574            var tag = defaultSingleLineContainerName;
   7575 
   7576            // "Block-extend the active range, and let new range be the
   7577            // result."
   7578            var newRange = blockExtend(getActiveRange());
   7579 
   7580            // "Let node list be a list of nodes, initially empty."
   7581            //
   7582            // "Append to node list the first node in tree order that is
   7583            // contained in new range and is an allowed child of "p", if any."
   7584            var nodeList = getContainedNodes(newRange, function(node) { return isAllowedChild(node, "p") })
   7585                .slice(0, 1);
   7586 
   7587            // "If node list is empty:"
   7588            if (!nodeList.length) {
   7589                // "If tag is not an allowed child of the active range's start
   7590                // node, return true."
   7591                if (!isAllowedChild(tag, getActiveRange().startContainer)) {
   7592                    return true;
   7593                }
   7594 
   7595                // "Set container to the result of calling createElement(tag)
   7596                // on the context object."
   7597                container = document.createElement(tag);
   7598 
   7599                // "Call insertNode(container) on the active range."
   7600                getActiveRange().insertNode(container);
   7601 
   7602                // "Call createElement("br") on the context object, and append
   7603                // the result as the last child of container."
   7604                container.appendChild(document.createElement("br"));
   7605 
   7606                // "Call collapse(container, 0) on the context object's
   7607                // Selection."
   7608                getSelection().collapse(container, 0);
   7609                getActiveRange().setStart(container, 0);
   7610                getActiveRange().setEnd(container, 0);
   7611 
   7612                // "Return true."
   7613                return true;
   7614            }
   7615 
   7616            // "While the nextSibling of the last member of node list is not
   7617            // null and is an allowed child of "p", append it to node list."
   7618            while (nodeList[nodeList.length - 1].nextSibling
   7619            && isAllowedChild(nodeList[nodeList.length - 1].nextSibling, "p")) {
   7620                nodeList.push(nodeList[nodeList.length - 1].nextSibling);
   7621            }
   7622 
   7623            // "Wrap node list, with sibling criteria returning false and new
   7624            // parent instructions returning the result of calling
   7625            // createElement(tag) on the context object. Set container to the
   7626            // result."
   7627            container = wrap(nodeList,
   7628                function() { return false },
   7629                function() { return document.createElement(tag) }
   7630            );
   7631        }
   7632 
   7633        // "If container's local name is "address", "listing", or "pre":"
   7634        if (container.tagName == "ADDRESS"
   7635        || container.tagName == "LISTING"
   7636        || container.tagName == "PRE") {
   7637            // "Let br be the result of calling createElement("br") on the
   7638            // context object."
   7639            var br = document.createElement("br");
   7640 
   7641            // "Call insertNode(br) on the active range."
   7642            getActiveRange().insertNode(br);
   7643 
   7644            // "Call collapse(node, offset + 1) on the context object's
   7645            // Selection."
   7646            getSelection().collapse(node, offset + 1);
   7647            getActiveRange().setStart(node, offset + 1);
   7648            getActiveRange().setEnd(node, offset + 1);
   7649 
   7650            // "If br is the last descendant of container, let br be the result
   7651            // of calling createElement("br") on the context object, then call
   7652            // insertNode(br) on the active range."
   7653            //
   7654            // Work around browser bugs: some browsers select the
   7655            // newly-inserted node, not per spec.
   7656            if (!isDescendant(nextNode(br), container)) {
   7657                getActiveRange().insertNode(document.createElement("br"));
   7658                getSelection().collapse(node, offset + 1);
   7659                getActiveRange().setEnd(node, offset + 1);
   7660            }
   7661 
   7662            // "Return true."
   7663            return true;
   7664        }
   7665 
   7666        // "If container's local name is "li", "dt", or "dd"; and either it has
   7667        // no children or it has a single child and that child is a br:"
   7668        if (["LI", "DT", "DD"].indexOf(container.tagName) != -1
   7669        && (!container.hasChildNodes()
   7670        || (container.childNodes.length == 1
   7671        && isHtmlElement(container.firstChild, "br")))) {
   7672            // "Split the parent of the one-node list consisting of container."
   7673            splitParent([container]);
   7674 
   7675            // "If container has no children, call createElement("br") on the
   7676            // context object and append the result as the last child of
   7677            // container."
   7678            if (!container.hasChildNodes()) {
   7679                container.appendChild(document.createElement("br"));
   7680            }
   7681 
   7682            // "If container is a dd or dt, and it is not an allowed child of
   7683            // any of its ancestors in the same editing host, set the tag name
   7684            // of container to the default single-line container name and let
   7685            // container be the result."
   7686            if (isHtmlElement(container, ["dd", "dt"])
   7687            && getAncestors(container).every(function(ancestor) {
   7688                return !inSameEditingHost(container, ancestor)
   7689                    || !isAllowedChild(container, ancestor)
   7690            })) {
   7691                container = setTagName(container, defaultSingleLineContainerName);
   7692            }
   7693 
   7694            // "Fix disallowed ancestors of container."
   7695            fixDisallowedAncestors(container);
   7696 
   7697            // "Return true."
   7698            return true;
   7699        }
   7700 
   7701        // "Let new line range be a new range whose start is the same as
   7702        // the active range's, and whose end is (container, length of
   7703        // container)."
   7704        var newLineRange = document.createRange();
   7705        newLineRange.setStart(getActiveRange().startContainer, getActiveRange().startOffset);
   7706        newLineRange.setEnd(container, getNodeLength(container));
   7707 
   7708        // "While new line range's start offset is zero and its start node is
   7709        // not a prohibited paragraph child, set its start to (parent of start
   7710        // node, index of start node)."
   7711        while (newLineRange.startOffset == 0
   7712        && !isProhibitedParagraphChild(newLineRange.startContainer)) {
   7713            newLineRange.setStart(newLineRange.startContainer.parentNode, getNodeIndex(newLineRange.startContainer));
   7714        }
   7715 
   7716        // "While new line range's start offset is the length of its start node
   7717        // and its start node is not a prohibited paragraph child, set its
   7718        // start to (parent of start node, 1 + index of start node)."
   7719        while (newLineRange.startOffset == getNodeLength(newLineRange.startContainer)
   7720        && !isProhibitedParagraphChild(newLineRange.startContainer)) {
   7721            newLineRange.setStart(newLineRange.startContainer.parentNode, 1 + getNodeIndex(newLineRange.startContainer));
   7722        }
   7723 
   7724        // "Let end of line be true if new line range contains either nothing
   7725        // or a single br, and false otherwise."
   7726        var containedInNewLineRange = getContainedNodes(newLineRange);
   7727        var endOfLine = !containedInNewLineRange.length
   7728            || (containedInNewLineRange.length == 1
   7729            && isHtmlElement(containedInNewLineRange[0], "br"));
   7730 
   7731        // "If the local name of container is "h1", "h2", "h3", "h4", "h5", or
   7732        // "h6", and end of line is true, let new container name be the default
   7733        // single-line container name."
   7734        var newContainerName;
   7735        if (/^H[1-6]$/.test(container.tagName)
   7736        && endOfLine) {
   7737            newContainerName = defaultSingleLineContainerName;
   7738 
   7739        // "Otherwise, if the local name of container is "dt" and end of line
   7740        // is true, let new container name be "dd"."
   7741        } else if (container.tagName == "DT"
   7742        && endOfLine) {
   7743            newContainerName = "dd";
   7744 
   7745        // "Otherwise, if the local name of container is "dd" and end of line
   7746        // is true, let new container name be "dt"."
   7747        } else if (container.tagName == "DD"
   7748        && endOfLine) {
   7749            newContainerName = "dt";
   7750 
   7751        // "Otherwise, let new container name be the local name of container."
   7752        } else {
   7753            newContainerName = container.tagName.toLowerCase();
   7754        }
   7755 
   7756        // "Let new container be the result of calling createElement(new
   7757        // container name) on the context object."
   7758        var newContainer = document.createElement(newContainerName);
   7759 
   7760        // "Copy all attributes of container to new container."
   7761        for (var i = 0; i < container.attributes.length; i++) {
   7762            newContainer.setAttributeNS(container.attributes[i].namespaceURI, container.attributes[i].name, container.attributes[i].value);
   7763        }
   7764 
   7765        // "If new container has an id attribute, unset it."
   7766        newContainer.removeAttribute("id");
   7767 
   7768        // "Insert new container into the parent of container immediately after
   7769        // container."
   7770        container.parentNode.insertBefore(newContainer, container.nextSibling);
   7771 
   7772        // "Let contained nodes be all nodes contained in new line range."
   7773        var containedNodes = getAllContainedNodes(newLineRange);
   7774 
   7775        // "Let frag be the result of calling extractContents() on new line
   7776        // range."
   7777        var frag = newLineRange.extractContents();
   7778 
   7779        // "Unset the id attribute (if any) of each Element descendant of frag
   7780        // that is not in contained nodes."
   7781        var descendants = getDescendants(frag);
   7782        for (var i = 0; i < descendants.length; i++) {
   7783            if (descendants[i].nodeType == Node.ELEMENT_NODE
   7784            && containedNodes.indexOf(descendants[i]) == -1) {
   7785                descendants[i].removeAttribute("id");
   7786            }
   7787        }
   7788 
   7789        // "Call appendChild(frag) on new container."
   7790        newContainer.appendChild(frag);
   7791 
   7792        // "While container's lastChild is a prohibited paragraph child, set
   7793        // container to its lastChild."
   7794        while (isProhibitedParagraphChild(container.lastChild)) {
   7795            container = container.lastChild;
   7796        }
   7797 
   7798        // "While new container's lastChild is a prohibited paragraph child,
   7799        // set new container to its lastChild."
   7800        while (isProhibitedParagraphChild(newContainer.lastChild)) {
   7801            newContainer = newContainer.lastChild;
   7802        }
   7803 
   7804        // "If container has no visible children, call createElement("br") on
   7805        // the context object, and append the result as the last child of
   7806        // container."
   7807        if (![].some.call(container.childNodes, isVisible)) {
   7808            container.appendChild(document.createElement("br"));
   7809        }
   7810 
   7811        // "If new container has no visible children, call createElement("br")
   7812        // on the context object, and append the result as the last child of
   7813        // new container."
   7814        if (![].some.call(newContainer.childNodes, isVisible)) {
   7815            newContainer.appendChild(document.createElement("br"));
   7816        }
   7817 
   7818        // "Call collapse(new container, 0) on the context object's Selection."
   7819        getSelection().collapse(newContainer, 0);
   7820        getActiveRange().setStart(newContainer, 0);
   7821        getActiveRange().setEnd(newContainer, 0);
   7822 
   7823        // "Return true."
   7824        return true;
   7825    }
   7826 };
   7827 
   7828 //@}
   7829 ///// The insertText command /////
   7830 //@{
   7831 commands.inserttext = {
   7832    action: function(value) {
   7833        // "Delete the selection, with strip wrappers false."
   7834        deleteSelection({stripWrappers: false});
   7835 
   7836        // "If the active range's start node is neither editable nor an editing
   7837        // host, return true."
   7838        if (!isEditable(getActiveRange().startContainer)
   7839        && !isEditingHost(getActiveRange().startContainer)) {
   7840            return true;
   7841        }
   7842 
   7843        // "If value's length is greater than one:"
   7844        if (value.length > 1) {
   7845            // "For each element el in value, take the action for the
   7846            // insertText command, with value equal to el."
   7847            for (var i = 0; i < value.length; i++) {
   7848                commands.inserttext.action(value[i]);
   7849            }
   7850 
   7851            // "Return true."
   7852            return true;
   7853        }
   7854 
   7855        // "If value is the empty string, return true."
   7856        if (value == "") {
   7857            return true;
   7858        }
   7859 
   7860        // "If value is a newline (U+00A0), take the action for the
   7861        // insertParagraph command and return true."
   7862        if (value == "\n") {
   7863            commands.insertparagraph.action();
   7864            return true;
   7865        }
   7866 
   7867        // "Let node and offset be the active range's start node and offset."
   7868        var node = getActiveRange().startContainer;
   7869        var offset = getActiveRange().startOffset;
   7870 
   7871        // "If node has a child whose index is offset − 1, and that child is a
   7872        // Text node, set node to that child, then set offset to node's
   7873        // length."
   7874        if (0 <= offset - 1
   7875        && offset - 1 < node.childNodes.length
   7876        && node.childNodes[offset - 1].nodeType == Node.TEXT_NODE) {
   7877            node = node.childNodes[offset - 1];
   7878            offset = getNodeLength(node);
   7879        }
   7880 
   7881        // "If node has a child whose index is offset, and that child is a Text
   7882        // node, set node to that child, then set offset to zero."
   7883        if (0 <= offset
   7884        && offset < node.childNodes.length
   7885        && node.childNodes[offset].nodeType == Node.TEXT_NODE) {
   7886            node = node.childNodes[offset];
   7887            offset = 0;
   7888        }
   7889 
   7890        // "Record current overrides, and let overrides be the result."
   7891        var overrides = recordCurrentOverrides();
   7892 
   7893        // "Call collapse(node, offset) on the context object's Selection."
   7894        getSelection().collapse(node, offset);
   7895        getActiveRange().setStart(node, offset);
   7896        getActiveRange().setEnd(node, offset);
   7897 
   7898        // "Canonicalize whitespace at (node, offset)."
   7899        canonicalizeWhitespace(node, offset);
   7900 
   7901        // "Let (node, offset) be the active range's start."
   7902        node = getActiveRange().startContainer;
   7903        offset = getActiveRange().startOffset;
   7904 
   7905        // "If node is a Text node:"
   7906        if (node.nodeType == Node.TEXT_NODE) {
   7907            // "Call insertData(offset, value) on node."
   7908            node.insertData(offset, value);
   7909 
   7910            // "Call collapse(node, offset) on the context object's Selection."
   7911            getSelection().collapse(node, offset);
   7912            getActiveRange().setStart(node, offset);
   7913 
   7914            // "Call extend(node, offset + 1) on the context object's
   7915            // Selection."
   7916            //
   7917            // Work around WebKit bug: the extend() can throw if the text we're
   7918            // adding is trailing whitespace.
   7919            try { getSelection().extend(node, offset + 1); } catch(e) {}
   7920            getActiveRange().setEnd(node, offset + 1);
   7921 
   7922        // "Otherwise:"
   7923        } else {
   7924            // "If node has only one child, which is a collapsed line break,
   7925            // remove its child from it."
   7926            //
   7927            // FIXME: IE incorrectly returns false here instead of true
   7928            // sometimes?
   7929            if (node.childNodes.length == 1
   7930            && isCollapsedLineBreak(node.firstChild)) {
   7931                node.removeChild(node.firstChild);
   7932            }
   7933 
   7934            // "Let text be the result of calling createTextNode(value) on the
   7935            // context object."
   7936            var text = document.createTextNode(value);
   7937 
   7938            // "Call insertNode(text) on the active range."
   7939            getActiveRange().insertNode(text);
   7940 
   7941            // "Call collapse(text, 0) on the context object's Selection."
   7942            getSelection().collapse(text, 0);
   7943            getActiveRange().setStart(text, 0);
   7944 
   7945            // "Call extend(text, 1) on the context object's Selection."
   7946            getSelection().extend(text, 1);
   7947            getActiveRange().setEnd(text, 1);
   7948        }
   7949 
   7950        // "Restore states and values from overrides."
   7951        restoreStatesAndValues(overrides);
   7952 
   7953        // "Canonicalize whitespace at the active range's start, with fix
   7954        // collapsed space false."
   7955        canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset, false);
   7956 
   7957        // "Canonicalize whitespace at the active range's end, with fix
   7958        // collapsed space false."
   7959        canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset, false);
   7960 
   7961        // "If value is a space character, autolink the active range's start."
   7962        if (/^[ \t\n\f\r]$/.test(value)) {
   7963            autolink(getActiveRange().startContainer, getActiveRange().startOffset);
   7964        }
   7965 
   7966        // "Call collapseToEnd() on the context object's Selection."
   7967        //
   7968        // Work around WebKit bug: sometimes it blows up the selection and
   7969        // throws, which we don't want.
   7970        try { getSelection().collapseToEnd(); } catch(e) {}
   7971        getActiveRange().collapse(false);
   7972 
   7973        // "Return true."
   7974        return true;
   7975    }
   7976 };
   7977 
   7978 //@}
   7979 ///// The insertUnorderedList command /////
   7980 //@{
   7981 commands.insertunorderedlist = {
   7982    preservesOverrides: true,
   7983    // "Toggle lists with tag name "ul", then return true."
   7984    action: function() { toggleLists("ul"); return true },
   7985    // "True if the selection's list state is "mixed" or "mixed ul", false
   7986    // otherwise."
   7987    indeterm: function() { return /^mixed( ul)?$/.test(getSelectionListState()) },
   7988    // "True if the selection's list state is "ul", false otherwise."
   7989    state: function() { return getSelectionListState() == "ul" },
   7990 };
   7991 
   7992 //@}
   7993 ///// The justifyCenter command /////
   7994 //@{
   7995 commands.justifycenter = {
   7996    preservesOverrides: true,
   7997    // "Justify the selection with alignment "center", then return true."
   7998    action: function() { justifySelection("center"); return true },
   7999    indeterm: function() {
   8000        // "Return false if the active range is null.  Otherwise, block-extend
   8001        // the active range. Return true if among visible editable nodes that
   8002        // are contained in the result and have no children, at least one has
   8003        // alignment value "center" and at least one does not. Otherwise return
   8004        // false."
   8005        if (!getActiveRange()) {
   8006            return false;
   8007        }
   8008        var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
   8009            return isEditable(node) && isVisible(node) && !node.hasChildNodes();
   8010        });
   8011        return nodes.some(function(node) { return getAlignmentValue(node) == "center" })
   8012            && nodes.some(function(node) { return getAlignmentValue(node) != "center" });
   8013    }, state: function() {
   8014        // "Return false if the active range is null.  Otherwise, block-extend
   8015        // the active range. Return true if there is at least one visible
   8016        // editable node that is contained in the result and has no children,
   8017        // and all such nodes have alignment value "center".  Otherwise return
   8018        // false."
   8019        if (!getActiveRange()) {
   8020            return false;
   8021        }
   8022        var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
   8023            return isEditable(node) && isVisible(node) && !node.hasChildNodes();
   8024        });
   8025        return nodes.length
   8026            && nodes.every(function(node) { return getAlignmentValue(node) == "center" });
   8027    }, value: function() {
   8028        // "Return the empty string if the active range is null.  Otherwise,
   8029        // block-extend the active range, and return the alignment value of the
   8030        // first visible editable node that is contained in the result and has
   8031        // no children. If there is no such node, return "left"."
   8032        if (!getActiveRange()) {
   8033            return "";
   8034        }
   8035        var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
   8036            return isEditable(node) && isVisible(node) && !node.hasChildNodes();
   8037        });
   8038        if (nodes.length) {
   8039            return getAlignmentValue(nodes[0]);
   8040        } else {
   8041            return "left";
   8042        }
   8043    },
   8044 };
   8045 
   8046 //@}
   8047 ///// The justifyFull command /////
   8048 //@{
   8049 commands.justifyfull = {
   8050    preservesOverrides: true,
   8051    // "Justify the selection with alignment "justify", then return true."
   8052    action: function() { justifySelection("justify"); return true },
   8053    indeterm: function() {
   8054        // "Return false if the active range is null.  Otherwise, block-extend
   8055        // the active range. Return true if among visible editable nodes that
   8056        // are contained in the result and have no children, at least one has
   8057        // alignment value "justify" and at least one does not. Otherwise
   8058        // return false."
   8059        if (!getActiveRange()) {
   8060            return false;
   8061        }
   8062        var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
   8063            return isEditable(node) && isVisible(node) && !node.hasChildNodes();
   8064        });
   8065        return nodes.some(function(node) { return getAlignmentValue(node) == "justify" })
   8066            && nodes.some(function(node) { return getAlignmentValue(node) != "justify" });
   8067    }, state: function() {
   8068        // "Return false if the active range is null.  Otherwise, block-extend
   8069        // the active range. Return true if there is at least one visible
   8070        // editable node that is contained in the result and has no children,
   8071        // and all such nodes have alignment value "justify".  Otherwise return
   8072        // false."
   8073        if (!getActiveRange()) {
   8074            return false;
   8075        }
   8076        var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
   8077            return isEditable(node) && isVisible(node) && !node.hasChildNodes();
   8078        });
   8079        return nodes.length
   8080            && nodes.every(function(node) { return getAlignmentValue(node) == "justify" });
   8081    }, value: function() {
   8082        // "Return the empty string if the active range is null.  Otherwise,
   8083        // block-extend the active range, and return the alignment value of the
   8084        // first visible editable node that is contained in the result and has
   8085        // no children. If there is no such node, return "left"."
   8086        if (!getActiveRange()) {
   8087            return "";
   8088        }
   8089        var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
   8090            return isEditable(node) && isVisible(node) && !node.hasChildNodes();
   8091        });
   8092        if (nodes.length) {
   8093            return getAlignmentValue(nodes[0]);
   8094        } else {
   8095            return "left";
   8096        }
   8097    },
   8098 };
   8099 
   8100 //@}
   8101 ///// The justifyLeft command /////
   8102 //@{
   8103 commands.justifyleft = {
   8104    preservesOverrides: true,
   8105    // "Justify the selection with alignment "left", then return true."
   8106    action: function() { justifySelection("left"); return true },
   8107    indeterm: function() {
   8108        // "Return false if the active range is null.  Otherwise, block-extend
   8109        // the active range. Return true if among visible editable nodes that
   8110        // are contained in the result and have no children, at least one has
   8111        // alignment value "left" and at least one does not. Otherwise return
   8112        // false."
   8113        if (!getActiveRange()) {
   8114            return false;
   8115        }
   8116        var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
   8117            return isEditable(node) && isVisible(node) && !node.hasChildNodes();
   8118        });
   8119        return nodes.some(function(node) { return getAlignmentValue(node) == "left" })
   8120            && nodes.some(function(node) { return getAlignmentValue(node) != "left" });
   8121    }, state: function() {
   8122        // "Return false if the active range is null.  Otherwise, block-extend
   8123        // the active range. Return true if there is at least one visible
   8124        // editable node that is contained in the result and has no children,
   8125        // and all such nodes have alignment value "left".  Otherwise return
   8126        // false."
   8127        if (!getActiveRange()) {
   8128            return false;
   8129        }
   8130        var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
   8131            return isEditable(node) && isVisible(node) && !node.hasChildNodes();
   8132        });
   8133        return nodes.length
   8134            && nodes.every(function(node) { return getAlignmentValue(node) == "left" });
   8135    }, value: function() {
   8136        // "Return the empty string if the active range is null.  Otherwise,
   8137        // block-extend the active range, and return the alignment value of the
   8138        // first visible editable node that is contained in the result and has
   8139        // no children. If there is no such node, return "left"."
   8140        if (!getActiveRange()) {
   8141            return "";
   8142        }
   8143        var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
   8144            return isEditable(node) && isVisible(node) && !node.hasChildNodes();
   8145        });
   8146        if (nodes.length) {
   8147            return getAlignmentValue(nodes[0]);
   8148        } else {
   8149            return "left";
   8150        }
   8151    },
   8152 };
   8153 
   8154 //@}
   8155 ///// The justifyRight command /////
   8156 //@{
   8157 commands.justifyright = {
   8158    preservesOverrides: true,
   8159    // "Justify the selection with alignment "right", then return true."
   8160    action: function() { justifySelection("right"); return true },
   8161    indeterm: function() {
   8162        // "Return false if the active range is null.  Otherwise, block-extend
   8163        // the active range. Return true if among visible editable nodes that
   8164        // are contained in the result and have no children, at least one has
   8165        // alignment value "right" and at least one does not. Otherwise return
   8166        // false."
   8167        if (!getActiveRange()) {
   8168            return false;
   8169        }
   8170        var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
   8171            return isEditable(node) && isVisible(node) && !node.hasChildNodes();
   8172        });
   8173        return nodes.some(function(node) { return getAlignmentValue(node) == "right" })
   8174            && nodes.some(function(node) { return getAlignmentValue(node) != "right" });
   8175    }, state: function() {
   8176        // "Return false if the active range is null.  Otherwise, block-extend
   8177        // the active range. Return true if there is at least one visible
   8178        // editable node that is contained in the result and has no children,
   8179        // and all such nodes have alignment value "right".  Otherwise return
   8180        // false."
   8181        if (!getActiveRange()) {
   8182            return false;
   8183        }
   8184        var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
   8185            return isEditable(node) && isVisible(node) && !node.hasChildNodes();
   8186        });
   8187        return nodes.length
   8188            && nodes.every(function(node) { return getAlignmentValue(node) == "right" });
   8189    }, value: function() {
   8190        // "Return the empty string if the active range is null.  Otherwise,
   8191        // block-extend the active range, and return the alignment value of the
   8192        // first visible editable node that is contained in the result and has
   8193        // no children. If there is no such node, return "left"."
   8194        if (!getActiveRange()) {
   8195            return "";
   8196        }
   8197        var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
   8198            return isEditable(node) && isVisible(node) && !node.hasChildNodes();
   8199        });
   8200        if (nodes.length) {
   8201            return getAlignmentValue(nodes[0]);
   8202        } else {
   8203            return "left";
   8204        }
   8205    },
   8206 };
   8207 
   8208 //@}
   8209 ///// The outdent command /////
   8210 //@{
   8211 commands.outdent = {
   8212    preservesOverrides: true,
   8213    action: function() {
   8214        // "Let items be a list of all lis that are ancestor containers of the
   8215        // range's start and/or end node."
   8216        //
   8217        // It's annoying to get this in tree order using functional stuff
   8218        // without doing getDescendants(document), which is slow, so I do it
   8219        // imperatively.
   8220        var items = [];
   8221        (function(){
   8222            for (
   8223                var ancestorContainer = getActiveRange().endContainer;
   8224                ancestorContainer != getActiveRange().commonAncestorContainer;
   8225                ancestorContainer = ancestorContainer.parentNode
   8226            ) {
   8227                if (isHtmlElement(ancestorContainer, "li")) {
   8228                    items.unshift(ancestorContainer);
   8229                }
   8230            }
   8231            for (
   8232                var ancestorContainer = getActiveRange().startContainer;
   8233                ancestorContainer;
   8234                ancestorContainer = ancestorContainer.parentNode
   8235            ) {
   8236                if (isHtmlElement(ancestorContainer, "li")) {
   8237                    items.unshift(ancestorContainer);
   8238                }
   8239            }
   8240        })();
   8241 
   8242        // "For each item in items, normalize sublists of item."
   8243        items.forEach(normalizeSublists);
   8244 
   8245        // "Block-extend the active range, and let new range be the result."
   8246        var newRange = blockExtend(getActiveRange());
   8247 
   8248        // "Let node list be a list of nodes, initially empty."
   8249        //
   8250        // "For each node node contained in new range, append node to node list
   8251        // if the last member of node list (if any) is not an ancestor of node;
   8252        // node is editable; and either node has no editable descendants, or is
   8253        // an ol or ul, or is an li whose parent is an ol or ul."
   8254        var nodeList = getContainedNodes(newRange, function(node) {
   8255            return isEditable(node)
   8256                && (!getDescendants(node).some(isEditable)
   8257                || isHtmlElement(node, ["ol", "ul"])
   8258                || (isHtmlElement(node, "li") && isHtmlElement(node.parentNode, ["ol", "ul"])));
   8259        });
   8260 
   8261        // "While node list is not empty:"
   8262        while (nodeList.length) {
   8263            // "While the first member of node list is an ol or ul or is not
   8264            // the child of an ol or ul, outdent it and remove it from node
   8265            // list."
   8266            while (nodeList.length
   8267            && (isHtmlElement(nodeList[0], ["OL", "UL"])
   8268            || !isHtmlElement(nodeList[0].parentNode, ["OL", "UL"]))) {
   8269                outdentNode(nodeList.shift());
   8270            }
   8271 
   8272            // "If node list is empty, break from these substeps."
   8273            if (!nodeList.length) {
   8274                break;
   8275            }
   8276 
   8277            // "Let sublist be a list of nodes, initially empty."
   8278            var sublist = [];
   8279 
   8280            // "Remove the first member of node list and append it to sublist."
   8281            sublist.push(nodeList.shift());
   8282 
   8283            // "While the first member of node list is the nextSibling of the
   8284            // last member of sublist, and the first member of node list is not
   8285            // an ol or ul, remove the first member of node list and append it
   8286            // to sublist."
   8287            while (nodeList.length
   8288            && nodeList[0] == sublist[sublist.length - 1].nextSibling
   8289            && !isHtmlElement(nodeList[0], ["OL", "UL"])) {
   8290                sublist.push(nodeList.shift());
   8291            }
   8292 
   8293            // "Record the values of sublist, and let values be the result."
   8294            var values = recordValues(sublist);
   8295 
   8296            // "Split the parent of sublist, with new parent null."
   8297            splitParent(sublist);
   8298 
   8299            // "Fix disallowed ancestors of each member of sublist."
   8300            sublist.forEach(fixDisallowedAncestors);
   8301 
   8302            // "Restore the values from values."
   8303            restoreValues(values);
   8304        }
   8305 
   8306        // "Return true."
   8307        return true;
   8308    }
   8309 };
   8310 
   8311 //@}
   8312 
   8313 //////////////////////////////////
   8314 ///// Miscellaneous commands /////
   8315 //////////////////////////////////
   8316 
   8317 ///// The defaultParagraphSeparator command /////
   8318 //@{
   8319 commands.defaultparagraphseparator = {
   8320    action: function(value) {
   8321        // "Let value be converted to ASCII lowercase. If value is then equal
   8322        // to "p" or "div", set the context object's default single-line
   8323        // container name to value and return true. Otherwise, return false."
   8324        value = value.toLowerCase();
   8325        if (value == "p" || value == "div") {
   8326            defaultSingleLineContainerName = value;
   8327            return true;
   8328        }
   8329        return false;
   8330    }, value: function() {
   8331        // "Return the context object's default single-line container name."
   8332        return defaultSingleLineContainerName;
   8333    },
   8334 };
   8335 
   8336 //@}
   8337 ///// The selectAll command /////
   8338 //@{
   8339 commands.selectall = {
   8340    // Note, this ignores the whole globalRange/getActiveRange() thing and
   8341    // works with actual selections.  Not suitable for autoimplementation.html.
   8342    action: function() {
   8343        // "Let target be the body element of the context object."
   8344        var target = document.body;
   8345 
   8346        // "If target is null, let target be the context object's
   8347        // documentElement."
   8348        if (!target) {
   8349            target = document.documentElement;
   8350        }
   8351 
   8352        // "If target is null, call getSelection() on the context object, and
   8353        // call removeAllRanges() on the result."
   8354        if (!target) {
   8355            getSelection().removeAllRanges();
   8356 
   8357        // "Otherwise, call getSelection() on the context object, and call
   8358        // selectAllChildren(target) on the result."
   8359        } else {
   8360            getSelection().selectAllChildren(target);
   8361        }
   8362 
   8363        // "Return true."
   8364        return true;
   8365    }
   8366 };
   8367 
   8368 //@}
   8369 ///// The styleWithCSS command /////
   8370 //@{
   8371 commands.stylewithcss = {
   8372    action: function(value) {
   8373        // "If value is an ASCII case-insensitive match for the string
   8374        // "false", set the CSS styling flag to false. Otherwise, set the
   8375        // CSS styling flag to true.  Either way, return true."
   8376        cssStylingFlag = String(value).toLowerCase() != "false";
   8377        return true;
   8378    }, state: function() { return cssStylingFlag }
   8379 };
   8380 
   8381 //@}
   8382 ///// The useCSS command /////
   8383 //@{
   8384 commands.usecss = {
   8385    action: function(value) {
   8386        // "If value is an ASCII case-insensitive match for the string "false",
   8387        // set the CSS styling flag to true. Otherwise, set the CSS styling
   8388        // flag to false.  Either way, return true."
   8389        cssStylingFlag = String(value).toLowerCase() == "false";
   8390        return true;
   8391    }
   8392 };
   8393 //@}
   8394 
   8395 // Some final setup
   8396 //@{
   8397 (function() {
   8398 // Opera 11.50 doesn't implement Object.keys, so I have to make an explicit
   8399 // temporary, which means I need an extra closure to not leak the temporaries
   8400 // into the global namespace.  >:(
   8401 var commandNames = [];
   8402 for (var command in commands) {
   8403    commandNames.push(command);
   8404 }
   8405 commandNames.forEach(function(command) {
   8406    // "If a command does not have a relevant CSS property specified, it
   8407    // defaults to null."
   8408    if (!("relevantCssProperty" in commands[command])) {
   8409        commands[command].relevantCssProperty = null;
   8410    }
   8411 
   8412    // "If a command has inline command activated values defined but nothing
   8413    // else defines when it is indeterminate, it is indeterminate if among
   8414    // formattable nodes effectively contained in the active range, there is at
   8415    // least one whose effective command value is one of the given values and
   8416    // at least one whose effective command value is not one of the given
   8417    // values."
   8418    if ("inlineCommandActivatedValues" in commands[command]
   8419    && !("indeterm" in commands[command])) {
   8420        commands[command].indeterm = function() {
   8421            if (!getActiveRange()) {
   8422                return false;
   8423            }
   8424 
   8425            var values = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)
   8426                .map(function(node) { return getEffectiveCommandValue(node, command) });
   8427 
   8428            var matchingValues = values.filter(function(value) {
   8429                return commands[command].inlineCommandActivatedValues.indexOf(value) != -1;
   8430            });
   8431 
   8432            return matchingValues.length >= 1
   8433                && values.length - matchingValues.length >= 1;
   8434        };
   8435    }
   8436 
   8437    // "If a command has inline command activated values defined, its state is
   8438    // true if either no formattable node is effectively contained in the
   8439    // active range, and the active range's start node's effective command
   8440    // value is one of the given values; or if there is at least one
   8441    // formattable node effectively contained in the active range, and all of
   8442    // them have an effective command value equal to one of the given values."
   8443    if ("inlineCommandActivatedValues" in commands[command]) {
   8444        commands[command].state = function() {
   8445            if (!getActiveRange()) {
   8446                return false;
   8447            }
   8448 
   8449            var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode);
   8450 
   8451            if (nodes.length == 0) {
   8452                return commands[command].inlineCommandActivatedValues
   8453                    .indexOf(getEffectiveCommandValue(getActiveRange().startContainer, command)) != -1;
   8454            } else {
   8455                return nodes.every(function(node) {
   8456                    return commands[command].inlineCommandActivatedValues
   8457                        .indexOf(getEffectiveCommandValue(node, command)) != -1;
   8458                });
   8459            }
   8460        };
   8461    }
   8462 
   8463    // "If a command is a standard inline value command, it is indeterminate if
   8464    // among formattable nodes that are effectively contained in the active
   8465    // range, there are two that have distinct effective command values. Its
   8466    // value is the effective command value of the first formattable node that
   8467    // is effectively contained in the active range; or if there is no such
   8468    // node, the effective command value of the active range's start node; or
   8469    // if that is null, the empty string."
   8470    if ("standardInlineValueCommand" in commands[command]) {
   8471        commands[command].indeterm = function() {
   8472            if (!getActiveRange()) {
   8473                return false;
   8474            }
   8475 
   8476            var values = getAllEffectivelyContainedNodes(getActiveRange())
   8477                .filter(isFormattableNode)
   8478                .map(function(node) { return getEffectiveCommandValue(node, command) });
   8479            for (var i = 1; i < values.length; i++) {
   8480                if (values[i] != values[i - 1]) {
   8481                    return true;
   8482                }
   8483            }
   8484            return false;
   8485        };
   8486 
   8487        commands[command].value = function() {
   8488            if (!getActiveRange()) {
   8489                return "";
   8490            }
   8491 
   8492            var refNode = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)[0];
   8493 
   8494            if (typeof refNode == "undefined") {
   8495                refNode = getActiveRange().startContainer;
   8496            }
   8497 
   8498            var ret = getEffectiveCommandValue(refNode, command);
   8499            if (ret === null) {
   8500                return "";
   8501            }
   8502            return ret;
   8503        };
   8504    }
   8505 
   8506    // "If a command preserves overrides, then before taking its action, the
   8507    // user agent must record current overrides. After taking the action, if
   8508    // the active range is collapsed, it must restore states and values from
   8509    // the recorded list."
   8510    if ("preservesOverrides" in commands[command]) {
   8511        var oldAction = commands[command].action;
   8512 
   8513        commands[command].action = function(value) {
   8514            var overrides = recordCurrentOverrides();
   8515            var ret = oldAction(value);
   8516            if (getActiveRange().collapsed) {
   8517                restoreStatesAndValues(overrides);
   8518            }
   8519            return ret;
   8520        };
   8521    }
   8522 });
   8523 })();
   8524 //@}
   8525 
   8526 // vim: foldmarker=@{,@} foldmethod=marker