text: // Licensed under the Apache License, Version 2.0 (the "License");
  // you may not use this file except in compliance with the License.
  // You may obtain a copy of the License at
  //
  //     http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing, software
  // distributed under the License is distributed on an "AS IS" BASIS,
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  // See the License for the specific language governing permissions and
  // limitations under the License.
  
  // Copyright 2006 Google Inc. All Rights Reserved.
  
  /**
   * @fileoverview Functions to style text.
   *
   */
  
  goog.provide('goog.editor.plugins.BasicTextFormatter');
  goog.provide('goog.editor.plugins.BasicTextFormatter.COMMAND');
  
  goog.require('goog.array');
  goog.require('goog.debug.Logger');
  goog.require('goog.dom');
  goog.require('goog.dom.NodeType');
  goog.require('goog.dom.TagIterator');
  goog.require('goog.dom.TagName');
  goog.require('goog.dom.TextRangeIterator');
  goog.require('goog.dom.classes');
  goog.require('goog.editor.BrowserFeature');
  goog.require('goog.editor.Plugin');
  goog.require('goog.editor.node');
  goog.require('goog.editor.range');
  goog.require('goog.iter');
  goog.require('goog.object');
  goog.require('goog.string');
  goog.require('goog.string.Unicode');
  goog.require('goog.style');
  goog.require('goog.userAgent');
  
  
  /**
   * Functions to style text (e.g. underline, make bold, etc.)
   * @constructor
   * @extends {goog.editor.Plugin}
   */
  goog.editor.plugins.BasicTextFormatter = function() {
    goog.editor.Plugin.call(this);
  };
  goog.inherits(goog.editor.plugins.BasicTextFormatter, goog.editor.Plugin);
  
  
  /** @inheritDoc */
  goog.editor.plugins.BasicTextFormatter.prototype.getTrogClassId = function() {
    return 'BTF';
  };
  
  
  /**
   * Logging object.
   * @type {goog.debug.Logger}
   * @protected
   * @override
   */
  goog.editor.plugins.BasicTextFormatter.prototype.logger =
      goog.debug.Logger.getLogger('goog.editor.plugins.BasicTextFormatter');
  
  
  /**
   * Commands implemented by this plugin.
   * @enum {string}
   */
  goog.editor.plugins.BasicTextFormatter.COMMAND = {
    INDENT: '+indent',
    OUTDENT: '+outdent',
    STRIKE_THROUGH: '+strikeThrough',
    HORIZONTAL_RULE: '+insertHorizontalRule',
    SUBSCRIPT: '+subscript',
    SUPERSCRIPT: '+superscript',
    UNDERLINE: '+underline',
    BOLD: '+bold',
    ITALIC: '+italic',
    FONT_SIZE: '+fontSize',
    FONT_FACE: '+fontName',
    FONT_COLOR: '+foreColor',
    BACKGROUND_COLOR: '+backColor',
    ORDERED_LIST: '+insertOrderedList',
    UNORDERED_LIST: '+insertUnorderedList',
    JUSTIFY_CENTER: '+justifyCenter',
    JUSTIFY_FULL: '+justifyFull',
    JUSTIFY_RIGHT: '+justifyRight',
    JUSTIFY_LEFT: '+justifyLeft'
  };
  
  
  /**
   * Inverse map of execCommand strings to
   * {@link goog.editor.plugins.BasicTextFormatter.COMMAND} constants. Used to
   * determine whether a string corresponds to a command this plugin
   * handles in O(1) time.
   * @type {Object}
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.SUPPORTED_COMMANDS_ =
      goog.object.transpose(goog.editor.plugins.BasicTextFormatter.COMMAND);
  
  
  /**
   * Whether the string corresponds to a command this plugin handles.
   * @param {string} command Command string to check.
   * @return {boolean} Whether the string corresponds to a command
   *     this plugin handles.
   */
  goog.editor.plugins.BasicTextFormatter.prototype.isSupportedCommand = function(
      command) {
    // TODO: restore this to simple check once table editing
    // is moved out into its own plugin
    return command in goog.editor.plugins.BasicTextFormatter.SUPPORTED_COMMANDS_;
  };
  
  
  /**
   * @return {goog.dom.AbstractRange} The closure range object that wraps the
   *     current user selection.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.getRange_ = function() {
    return this.fieldObject.getRange();
  };
  
  
  /**
   * @return {Document} The document object associated with the currently active
   *     field.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.getDocument_ = function() {
    return this.getFieldDomHelper().getDocument();
  };
  
  
  /**
   * Execute a user-initiated command.
   * @param {goog.editor.plugins.BasicTextFormatter.COMMAND} command Command
   *     to execute.
   * @param {string|number|boolean|null} var_args For color commands, this
   *     should be the hex color (with the #). It will be unused for
   *     other commands.
   * @return {Object|undefined} The result of the command.
   */
  goog.editor.plugins.BasicTextFormatter.prototype.execCommandInternal = function(
      command, var_args) {
    var preserveDir, styleWithCss, needsFormatBlockDiv, hasDummySelection;
    var result;
    var opt_arg = arguments[1];
  
    switch (command) {
      case goog.editor.plugins.BasicTextFormatter.COMMAND.BACKGROUND_COLOR:
        // Don't bother for no color selected, color picker is resetting itself.
        if (!goog.isNull(opt_arg)) {
          if (goog.editor.BrowserFeature.EATS_EMPTY_BACKGROUND_COLOR) {
            this.applyBgColorManually_(opt_arg);
          } else if (goog.userAgent.OPERA) {
            // backColor will color the block level element instead of
            // the selected span of text in Opera.
            this.execCommandHelper_('hiliteColor', opt_arg);
          } else {
            this.execCommandHelper_(command, opt_arg);
          }
        }
        break;
  
      case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_CENTER:
      case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_FULL:
      case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_RIGHT:
      case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_LEFT:
        this.justify_(command);
        break;
  
      default:
        if (command ==
            goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_COLOR &&
            goog.isNull(opt_arg)) {
          // If we don't have a color, then FONT_COLOR is a no-op.
          break;
        }
  
        switch (command) {
          case goog.editor.plugins.BasicTextFormatter.COMMAND.INDENT:
          case goog.editor.plugins.BasicTextFormatter.COMMAND.OUTDENT:
            if (goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS) {
              if (goog.userAgent.GECKO) {
                styleWithCss = true;
              }
              if (goog.userAgent.OPERA) {
                if (command ==
                    goog.editor.plugins.BasicTextFormatter.COMMAND.OUTDENT) {
                  // styleWithCSS actually sets negative margins on <blockquote>
                  // to outdent them. If the command is enabled without
                  // styleWithCSS flipped on, then the caret is in a blockquote so
                  // styleWithCSS must not be used. But if the command is not
                  // enabled, styleWithCSS should be used so that elements such as
                  // a <div> with a margin-left style can still be outdented.
                  // (Opera bug: CORE-21118)
                  styleWithCss =
                      !this.getDocument_().queryCommandEnabled('outdent');
                } else {
                  // Always use styleWithCSS for indenting. Otherwise, Opera will
                  // make separate <blockquote>s around *each* indented line,
                  // which adds big default <blockquote> margins between each
                  // indented line.
                  styleWithCss = true;
                }
              }
            }
            // Fall through.
  
          case goog.editor.plugins.BasicTextFormatter.COMMAND.ORDERED_LIST:
          case goog.editor.plugins.BasicTextFormatter.COMMAND.UNORDERED_LIST:
            if (goog.editor.BrowserFeature.LEAVES_P_WHEN_REMOVING_LISTS &&
                this.queryCommandStateInternal_(this.getDocument_(),
                    command)) {
              // IE leaves behind P tags when unapplying lists.
              // If we're not in P-mode, then we want divs
              // So, unlistify, then convert the Ps into divs.
              needsFormatBlockDiv = this.fieldObject.queryCommandValue(
                  goog.editor.Command.DEFAULT_TAG) != goog.dom.TagName.P;
            } else if (!goog.editor.BrowserFeature.CAN_LISTIFY_BR) {
              // IE doesn't convert BRed line breaks into separate list items.
              // So convert the BRs to divs, then do the listify.
              this.convertBreaksToDivs_();
            }
  
            // This fix only works in Gecko.
            if (goog.userAgent.GECKO &&
                goog.editor.BrowserFeature.FORGETS_FORMATTING_WHEN_LISTIFYING &&
                !this.queryCommandValue(command)) {
              hasDummySelection |= this.beforeInsertListGecko_();
            }
  
            // Both FF & IE may lose directionality info. Save/restore it.
            // TODO: Does Safari also need this?
            // TODO (gmark, jparent): This isn't ideal because it uses a string
            // literal, so if the plugin name changes, it would break. We need a
            // better solution. See also other places in code that use
            // this.getPluginByClassId('Bidi').
            preserveDir = !!this.fieldObject.getPluginByClassId('Bidi');
            break;
  
          case goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT:
          case goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT:
            if (goog.editor.BrowserFeature.NESTS_SUBSCRIPT_SUPERSCRIPT) {
              // This browser nests subscript and superscript when both are
              // applied, instead of canceling out the first when applying the
              // second.
              this.applySubscriptSuperscriptWorkarounds_(command);
            }
            break;
  
          case goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE:
          case goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD:
          case goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC:
           // If we are applying the formatting, then we want to have styleWithCSS
           // false so that we generate html tags (like <b>).  If we are
           // unformatting something, we want to have styleWithCSS true so that we
           // can unformat both html tags and inline styling.
           // TODO: What about WebKit and Opera?
           styleWithCss = goog.userAgent.GECKO &&
                          goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS &&
                          this.queryCommandValue(command);
           break;
  
          // It is very expensive in FF (order of magnitude difference) to use
          // font tags instead of styled spans. Whenever possible,
          // force FF to use spans.
          // Font size is very expensive too, but FF always uses font tags,
          // regardless of which styleWithCSS value you use.
          case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_COLOR:
          case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_FACE:
            styleWithCss = goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS &&
                           goog.userAgent.GECKO;
        }
  
        /**
         * Cases where we just use the default execCommand (in addition
         * to the above fall-throughs)
         * goog.editor.plugins.BasicTextFormatter.COMMAND.STRIKE_THROUGH:
         * goog.editor.plugins.BasicTextFormatter.COMMAND.HORIZONTAL_RULE:
         * goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT:
         * goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT:
         * goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE:
         * goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD:
         * goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC:
         * goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_SIZE:
         * goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_FACE:
         */
        this.execCommandHelper_(command, opt_arg, preserveDir, styleWithCss);
  
        if (hasDummySelection) {
          this.getDocument_().execCommand('Delete', false, true);
        }
  
        if (needsFormatBlockDiv) {
          this.getDocument_().execCommand('FormatBlock', false, '<div>');
        }
    }
    // FF loses focus, so we have to set the focus back to the document or the
    // user can't type after selecting from menu.  In IE, focus is set correctly
    // and resetting it here messes it up.
    if (goog.userAgent.GECKO) {
      this.getFieldDomHelper().getWindow().focus();
    }
    return result;
  };
  
  
  /**
   * Gets the command value.
   * @param {string} command The command value to get.
   * @return {string|boolean|null} The current value of the command in the given
   *     selection.  NOTE: This return type list is not documented in MSDN or MDC
   *     and has been constructed from experience.  Please update it
   *     if necessary.
   */
  goog.editor.plugins.BasicTextFormatter.prototype.queryCommandValue = function(
      command) {
    var styleWithCss;
    switch (command) {
      case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_CENTER:
      case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_FULL:
      case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_RIGHT:
      case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_LEFT:
        return this.isJustification_(command);
  
      case goog.editor.plugins.BasicTextFormatter.COMMAND.INDENT:
      case goog.editor.plugins.BasicTextFormatter.COMMAND.OUTDENT:
      case goog.editor.plugins.BasicTextFormatter.COMMAND.HORIZONTAL_RULE:
        // TODO: See if there are reasonable results to return for
        // these commands.
        return false;
  
      case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_SIZE:
      case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_FACE:
      case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_COLOR:
      case goog.editor.plugins.BasicTextFormatter.COMMAND.BACKGROUND_COLOR:
        // We use queryCommandValue here since we don't just want to know if a
        // color/fontface/fontsize is applied, we want to know WHICH one it is.
        return this.queryCommandValueInternal_(this.getDocument_(), command,
            goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS &&
            goog.userAgent.GECKO);
  
      case goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE:
      case goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD:
      case goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC:
        styleWithCss = goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS &&
                       goog.userAgent.GECKO;
  
      default:
        /**
         * goog.editor.plugins.BasicTextFormatter.COMMAND.STRIKE_THROUGH
         * goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT
         * goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT
         * goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE
         * goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD
         * goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC
         * goog.editor.plugins.BasicTextFormatter.COMMAND.ORDERED_LIST
         * goog.editor.plugins.BasicTextFormatter.COMMAND.UNORDERED_LIST
         */
        // This only works for commands that use the default execCommand
        return this.queryCommandStateInternal_(this.getDocument_(), command,
            styleWithCss);
    }
  };
  
  
  /**
   * @inheritDoc
   */
  goog.editor.plugins.BasicTextFormatter.prototype.prepareContentsHtml =
      function(html) {
    // If the browser collapses empty nodes and the field has only a script
    // tag in it, then it will collapse this node. Which will mean the user
    // can't click into it to edit it.
    if (goog.editor.BrowserFeature.COLLAPSES_EMPTY_NODES &&
        html.match(/^\s*<script/i)) {
      html = '&nbsp;' + html;
    }
  
    if (goog.editor.BrowserFeature.CONVERT_TO_B_AND_I_TAGS) {
      // Some browsers (FF) can't undo strong/em in some cases, but can undo b/i!
      html = html.replace(/<(\/?)strong([^\w])/gi, '<$1b$2');
      html = html.replace(/<(\/?)em([^\w])/gi, '<$1i$2');
    }
  
    return html;
  };
  
  
  /**
   * @inheritDoc
   */
  goog.editor.plugins.BasicTextFormatter.prototype.cleanContentsDom =
      function(fieldCopy) {
    var images = goog.dom.$$(goog.dom.TagName.IMG, null, fieldCopy);
    for (var i = 0, image; image = images[i]; i++) {
      if (goog.editor.BrowserFeature.SHOWS_CUSTOM_ATTRS_IN_INNER_HTML) {
        // Only need to remove these attributes in IE because
        // Firefox and Safari don't show custom attributes in the innerHTML.
        image.removeAttribute('tabIndex');
        image.removeAttribute('tabIndexSet');
        goog.removeHashCode(image);
  
        // oldTabIndex will only be set if
        // goog.editor.BrowserFeature.TABS_THROUGH_IMAGES is true
        // and we're on P-on-enter mode.
        if (image.oldTabIndex) {
          image.tabIndex = image.oldTabIndex;
        }
      }
    }
  };
  
  
  /**
   * @inheritDoc
   */
  goog.editor.plugins.BasicTextFormatter.prototype.cleanContentsHtml =
      function(html) {
    if (goog.editor.BrowserFeature.MOVES_STYLE_TO_HEAD) {
      // Safari creates a new <head> element for <style> tags, so prepend their
      // contents to the output.
      var heads = this.fieldObject.getEditableDomHelper().$$(
          goog.dom.TagName.HEAD);
      var stylesHtmlArr = [];
  
      // i starts at 1 so we don't copy in the original, legitimate <head>.
      var numHeads = heads.length;
      for (var i = 1; i < numHeads; ++i) {
        var styles = goog.dom.$$(goog.dom.TagName.STYLE, null, heads[i]);
        var numStyles = styles.length;
        for (var j = 0; j < numStyles; ++j) {
          stylesHtmlArr.push(styles[j].outerHTML);
        }
      }
      return stylesHtmlArr.join('') + html;
    }
  
    return html;
  };
  
  
  /**
   * @inheritDoc
   */
  goog.editor.plugins.BasicTextFormatter.prototype.handleKeyboardShortcut =
      function(e, key, isModifierPressed) {
    if (!isModifierPressed) {
      return false;
    }
    var command;
    switch (key) {
      case 'b': // Ctrl+B
        command = goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD;
        break;
      case 'i': // Ctrl+I
        command = goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC;
        break;
      case 'u': // Ctrl+U
        command = goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE;
        break;
      // TODO: This doesn't belong in here.  Clients should handle
      // this themselves.
      // Catching control + s prevents the annoying browser save dialog
      // from appearing.
      case 's': // Ctrl+S
        return true;
    }
  
    if (command) {
      this.fieldObject.execCommand(command);
      return true;
    }
  
    return false;
  };
  
  
  // Helpers for execCommand
  
  /**
   * Regular expression to match BRs in HTML. Saves the BRs' attributes in $1 for
   * use with replace(). In non-IE browsers, does not match BRs adjacent to an
   * opening or closing DIV or P tag, since nonrendered BR elements can occur at
   * the end of block level containers in those browsers' editors.
   * @type {RegExp}
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.BR_REGEXP_ =
      goog.userAgent.IE ? /<br([^\/>]*)\/?>/gi :
                          /<br([^\/>]*)\/?>(?!<\/(div|p)>)/gi;
  
  /**
   * Convert BRs in the selection to divs.
   * This is only intended to be used in IE and Opera.
   * @return {boolean} Whether any BR's were converted.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.convertBreaksToDivs_ =
      function() {
    if (!goog.userAgent.IE && !goog.userAgent.OPERA) {
      // This function is only supported on IE and Opera.
      return false;
    }
    var range = this.getRange_();
    var parent = range.getContainerElement();
    var doc = this.getDocument_();
  
    goog.editor.plugins.BasicTextFormatter.BR_REGEXP_.lastIndex = 0;
    // Only mess with the HTML/selection if it contains a BR.
    if (goog.editor.plugins.BasicTextFormatter.BR_REGEXP_.test(
            parent.innerHTML)) {
      // Insert temporary markers to remember the selection.
      var savedRange = range.saveUsingCarets();
  
      if (parent.tagName == goog.dom.TagName.P) {
        // Can't append paragraphs to paragraph tags. Throws an exception in IE.
        goog.editor.plugins.BasicTextFormatter.convertParagraphToDiv_(
            parent, true);
      } else {
        // Used to do:
        // IE: <div>foo<br>bar</div> --> <div>foo<p id="temp_br">bar</div>
        // Opera: <div>foo<br>bar</div> --> <div>foo<p class="temp_br">bar</div>
        // To fix bug 1939883, now does for both:
        // <div>foo<br>bar</div> --> <div>foo<p trtempbr="temp_br">bar</div>
        // TODO: Confirm if there's any way to skip this
        // intermediate step of converting br's to p's before converting those to
        // div's. The reason may be hidden in CLs 5332866 and 8530601.
        var attribute = 'trtempbr';
        var value = 'temp_br';
        parent.innerHTML = parent.innerHTML.replace(
            goog.editor.plugins.BasicTextFormatter.BR_REGEXP_,
            '<p$1 ' + attribute + '="' + value + '">');
  
        var paragraphs =
            goog.array.toArray(parent.getElementsByTagName(goog.dom.TagName.P));
        goog.iter.forEach(paragraphs, function(paragraph) {
          if (paragraph.getAttribute(attribute) == value) {
            paragraph.removeAttribute(attribute);
            if (goog.string.isBreakingWhitespace(
                    goog.dom.getTextContent(paragraph))) {
              // Prevent the empty blocks from collapsing.
              // A <BR> is preferable because it doesn't result in any text being
              // added to the "blank" line. In IE, however, it is possible to
              // place the caret after the <br>, which effectively creates a
              // visible line break. Because of this, we have to resort to using a
              // &nbsp; in IE.
              var child = goog.userAgent.IE ?
                          doc.createTextNode(goog.string.Unicode.NBSP) :
                          doc.createElement(goog.dom.TagName.BR);
              paragraph.appendChild(child);
            }
            goog.editor.plugins.BasicTextFormatter.convertParagraphToDiv_(
                paragraph);
          }
        });
      }
  
      // Select the previously selected text so we only listify
      // the selected portion and maintain the user's selection.
      savedRange.restore();
      return true;
    }
  
    return false;
  };
  
  
  /**
   * Convert the given paragraph to being a div. This clobbers the
   * passed-in node!
   * This is only intended to be used in IE and Opera.
   * @param {Node} paragraph Paragragh to convert to a div.
   * @param {boolean} opt_convertBrs If true, also convert BRs to divs.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.convertParagraphToDiv_ =
      function(paragraph, opt_convertBrs) {
    if (!goog.userAgent.IE && !goog.userAgent.OPERA) {
      // This function is only supported on IE and Opera.
      return;
    }
    var outerHTML = paragraph.outerHTML.replace(/<(\/?)p/gi, '<$1div');
    if (opt_convertBrs) {
      // IE fills in the closing div tag if it's missing!
      outerHTML = outerHTML.replace(
          goog.editor.plugins.BasicTextFormatter.BR_REGEXP_,
          '</div><div$1>');
    }
    if (goog.userAgent.OPERA && !/<\/div>$/i.test(outerHTML)) {
      // Opera doesn't automatically add the closing tag, so add it if needed.
      outerHTML += '</div>';
    }
    paragraph.outerHTML = outerHTML;
  };
  
  
  /**
   * If this is a goog.editor.plugins.BasicTextFormatter.COMMAND,
   * convert it to something that we can pass into execCommand,
   * queryCommandState, etc.
   *
   * TODO: Consider doing away with the + and converter completely.
   *
   * @param {goog.editor.plugins.BasicTextFormatter.COMMAND|string}
   *     command A command key.
   * @return {string} The equivalent execCommand command.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.convertToRealExecCommand_ = function(
      command) {
    return command.indexOf('+') == 0 ? command.substring(1) : command;
  };
  
  
  /**
   * Justify the text in the selection.
   * @param {string} command The type of justification to perform.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.justify_ = function(command) {
    this.execCommandHelper_(command, null, false, true);
    // Firefox cannot justify divs.  In fact, justifying divs results in removing
    // the divs and replacing them with brs.  So "<div>foo</div><div>bar</div>"
    // becomes "foo<br>bar" after alignment is applied.  However, if you justify
    // again, then you get "<div style='text-align: right'>foo<br>bar</div>",
    // which at least looks visually correct.  Since justification is (normally)
    // idempotent, it isn't a problem when the selection does not contain divs to
    // apply justifcation again.
    if (goog.userAgent.GECKO) {
      this.execCommandHelper_(command, null, false, true);
    }
  
    // Convert all block elements in the selection to use CSS text-align
    // instead of the align property. This works better because the align
    // property is overridden by the CSS text-align property.
    //
    // Only for browsers that can't handle this by the styleWithCSS execCommand,
    // which allows us to specify if we should insert align or text-align.
    // TODO: What about WebKit or Opera?
    if (!(goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS &&
          goog.userAgent.GECKO)) {
      goog.iter.forEach(this.fieldObject.getRange(),
          goog.editor.plugins.BasicTextFormatter.convertContainerToTextAlign_);
    }
  };
  
  
  /**
   * Converts the block element containing the given node to use CSS text-align
   * instead of the align property.
   * @param {Node} node The node to convert the container of.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.convertContainerToTextAlign_ =
      function(node) {
    var container = goog.editor.style.getContainer(node);
  
    // TODO: Fix this so that it doesn't screw up tables.
    if (container.align) {
      container.style.textAlign = container.align;
      container.removeAttribute('align');
    }
  };
  
  
  /**
   * Perform an execCommand on the active document.
   * @param {string} command The command to execute.
   * @param {string|number|boolean|null} opt_value Optional value.
   * @param {boolean} opt_preserveDir Set true to make sure that command does not
   *     change directionality of the selected text (works only if all selected
   *     text has the same directionality, otherwise ignored). Should not be true
   *     if bidi plugin is not loaded.
   * @param {boolean} opt_styleWithCss Set to true to ask the browser to use CSS
   *     to perform the execCommand.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.execCommandHelper_ = function(
      command, opt_value, opt_preserveDir, opt_styleWithCss) {
    // There is a bug in FF: some commands do not preserve attributes of the
    // block-level elements they replace.
    // This (among the rest) leads to loss of directionality information.
    // For now we use a hack (when opt_preserveDir==true) to avoid this
    // directionality problem in the simplest cases.
    // Known affected commands: formatBlock, insertOrderedList,
    // insertUnorderedList, indent, outdent.
    // A similar problem occurs in IE when insertOrderedList or
    // insertUnorderedList remove existing list.
    var dir = null;
    if (opt_preserveDir) {
      dir =
          this.fieldObject.queryCommandValue(
              goog.editor.Command.DIR_RTL) ? 'rtl' :
          this.fieldObject.queryCommandValue(
              goog.editor.Command.DIR_LTR) ? 'ltr' :
          null;
    }
  
    command = goog.editor.plugins.BasicTextFormatter.convertToRealExecCommand_(
        command);
  
    var endDiv, nbsp;
    if (goog.userAgent.IE) {
      var ret = this.applyExecCommandIEFixes_(command);
      endDiv = ret[0];
      nbsp = ret[1];
    }
  
    if (goog.userAgent.WEBKIT) {
      endDiv = this.applyExecCommandSafariFixes_(command);
    }
  
    if (goog.userAgent.GECKO) {
      this.applyExecCommandGeckoFixes_(command);
    }
  
    if (goog.editor.BrowserFeature.DOESNT_OVERRIDE_FONT_SIZE_IN_STYLE_ATTR &&
        command.toLowerCase() == 'fontsize') {
      this.removeFontSizeFromStyleAttrs_();
    }
  
    var doc = this.getDocument_();
    if (opt_styleWithCss &&
        goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS) {
      doc.execCommand('styleWithCSS', false, true);
      if (goog.userAgent.OPERA) {
        this.invalidateInlineCss_();
      }
    }
  
    doc.execCommand(command, false, opt_value);
    if (opt_styleWithCss &&
        goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS) {
      // If we enabled styleWithCSS, turn it back off.
      doc.execCommand('styleWithCSS', false, false);
    }
  
    if (goog.userAgent.WEBKIT && !goog.userAgent.isVersion('526') &&
        command.toLowerCase() == 'formatblock' &&
        opt_value && /^[<]?h\d[>]?$/i.test(opt_value)) {
      this.cleanUpSafariHeadings_();
    }
  
    if (/insert(un)?orderedlist/i.test(command)) {
      // NOTE: This doesn't check queryCommandState because it seems to
      // lie. Also, this runs for insertunorderedlist so that the the list
      // isn't made up of an <ul> for each <li> - even though it looks the same,
      // the markup is disgusting.
      if (goog.userAgent.WEBKIT) {
        this.fixSafariLists_();
      }
      if (goog.userAgent.IE) {
        this.fixIELists_();
  
        if (nbsp) {
          // Remove the text node, if applicable.  Do not try to instead clobber
          // the contents of the text node if it was added, or the same invalid
          // node thing as above will happen.  The error won't happen here, it
          // will happen after you hit enter and then do anything that loops
          // through the dom and tries to read that node.
          goog.dom.removeNode(nbsp);
        }
      }
    }
  
    if (endDiv) {
      // Remove the dummy div.
      goog.dom.removeNode(endDiv);
    }
  
    // Restore directionality if required and only when unambigous (dir!=null).
    if (dir) {
      this.fieldObject.execCommand(dir);
    }
  };
  
  
  /**
   * Applies a background color to a selection when the browser can't do the job.
   *
   * NOTE: If you think this is hacky, you should try applying
   * background color in Opera. It made me cry.
   *
   * @param {string} bgColor backgroundColor from .formatText to .execCommand.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.applyBgColorManually_ =
      function(bgColor) {
    var needsSpaceInTextNode = goog.userAgent.GECKO;
    var range = this.fieldObject.getRange();
    var textNode;
    var parentTag;
    if (range && range.isCollapsed()) {
      // Hack to handle Firefox bug:
      // https://bugzilla.mozilla.org/show_bug.cgi?id=279330
      // execCommand hiliteColor in Firefox on collapsed selection creates
      // a font tag onkeypress
      textNode = this.getFieldDomHelper().
          createTextNode(needsSpaceInTextNode ? ' ' : '');
  
      var containerNode = range.getStartNode();
      // Check if we're inside a tag that contains the cursor and nothing else;
      // if we are, don't create a dummySpan. Just use this containing tag to
      // hide the 1-space selection.
      // If the user sets a background color on a collapsed selection, then sets
      // another one immediately, we get a span tag with a single empty TextNode.
      // If the user sets a background color, types, then backspaces, we get a
      // span tag with nothing inside it (container is the span).
      parentTag = containerNode.nodeType == goog.dom.NodeType.ELEMENT ?
          containerNode : containerNode.parentNode;
  
      if (parentTag.innerHTML == '') {
        // There's an Element to work with
        // make the space character invisible using a CSS indent hack
        parentTag.style.textIndent = '-10000px';
        parentTag.appendChild(textNode);
      } else {
        // No Element to work with; make one
        // create a span with a space character inside
        // make the space character invisible using a CSS indent hack
        parentTag = this.getFieldDomHelper().createDom('span',
            {'style': 'text-indent:-10000px'}, textNode);
        range.replaceContentsWithNode(parentTag);
      }
      goog.dom.Range.createFromNodeContents(textNode).select();
    }
  
    this.execCommandHelper_('hiliteColor', bgColor, false, true);
  
    if (textNode) {
      // eliminate the space if necessary.
      if (needsSpaceInTextNode) {
        textNode.data = '';
      }
  
      // eliminate the hack.
      parentTag.style.textIndent = '';
      // execCommand modified our span so we leave it in place.
    }
  };
  
  
  //---------------------------------------------------------------------
  // browser fixes
  
  /**
   * The following execCommands are "broken" in some way - in IE they allow
   * the nodes outside the contentEditable region to get modified (see
   * execCommand below for more details).
   * @const
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.brokenExecCommandsIE_ = {
    'indent' : 1,
    'outdent' : 1,
    'insertOrderedList' : 1,
    'insertUnorderedList' : 1,
    'justifyCenter' : 1,
    'justifyFull' : 1,
    'justifyRight': 1,
    'justifyLeft': 1,
    'ltr' : 1,
    'rtl' : 1
  };
  
  
  /**
   * When the following commands are executed while the selection is
   * inside a blockquote, they hose the blockquote tag in weird and
   * unintuitive ways.
   * @const
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.blockquoteHatingCommandsIE_ = {
    'insertOrderedList' : 1,
    'insertUnorderedList' : 1
  };
  
  
  /**
   * Makes sure that superscript is removed before applying subscript, and vice
   * versa. Fixes {@link http://buganizer/issue?id=1173491} .
   * @param {goog.editor.plugins.BasicTextFormatter.COMMAND} command The command
   *     being applied, either SUBSCRIPT or SUPERSCRIPT.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.
      prototype.applySubscriptSuperscriptWorkarounds_ =
          function(command) {
    if (!this.queryCommandValue(command)) {
      // The current selection doesn't currently have the requested
      // command, so we are applying it as opposed to removing it.
      // (Note that queryCommandValue() will only return true if the
      // command is applied to the whole selection, not just part of it.
      // In this case it is fine because only if the whole selection has
      // the command applied will we be removing it and thus skipping the
      // removal of the opposite command.)
      var oppositeCommand =
          (command == goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT ?
              goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT :
              goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT);
      var oppositeExecCommand = goog.editor.plugins.BasicTextFormatter.
          convertToRealExecCommand_(oppositeCommand);
      // Executing the opposite command on a selection that already has it
      // applied will cancel it out. But if the selection only has the
      // opposite command applied to a part of it, the browser will
      // normalize the selection to have the opposite command applied on
      // the whole of it.
      if (!this.queryCommandValue(oppositeCommand)) {
        // The selection doesn't have the opposite command applied to the
        // whole of it, so let's exec the opposite command to normalize
        // the selection.
        // Note: since we know both subscript and superscript commands
        // will boil down to a simple call to the browser's execCommand(),
        // for performance reasons we can do that directly instead of
        // calling execCommandHelper_(). However this is a potential for
        // bugs if the implementation of execCommandHelper_() is changed
        // to do something more int eh case of subscript and superscript.
        this.getDocument_().execCommand(oppositeExecCommand, false, null);
      }
      // Now that we know the whole selection has the opposite command
      // applied, we exec it a second time to properly remove it.
      this.getDocument_().execCommand(oppositeExecCommand, false, null);
    }
  };
  
  
  /**
   * Removes inline font-size styles from elements fully contained in the
   * selection, so the font tags produced by execCommand work properly.
   * See {@bug 1286408}.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.removeFontSizeFromStyleAttrs_ =
      function() {
    // Expand the range so that we consider surrounding tags. E.g. if only the
    // text node inside a span is selected, the browser could wrap a font tag
    // around the span and leave the selection such that only the text node is
    // found when looking inside the range, not the span.
    var range = goog.editor.range.expand(this.fieldObject.getRange(),
                                         this.fieldObject.getElement());
    goog.iter.forEach(goog.iter.filter(range, function(tag, dummy, iter) {
      return iter.isStartTag() && range.containsNode(tag);
    }), function(node) {
      goog.style.setStyle(node, 'font-size', '');
      // Gecko doesn't remove empty style tags.
      if (goog.userAgent.GECKO &&
          node.style.length == 0 && node.getAttribute('style') != null) {
        node.removeAttribute('style');
      }
    });
  };
  
  
  /**
   * Apply pre-execCommand fixes for IE.
   * @param {string} command The command to execute.
   * @return {Array.<Node>} Array of nodes to be removed after the execCommand.
   *     Will never be longer than 2 elements.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.applyExecCommandIEFixes_ =
      function(command) {
    // IE has a crazy bug where executing list commands
    // around blockquotes cause the blockquotes to get transformed
    // into "<OL><OL>" or "<UL><UL>" tags.
    var toRemove = [];
    var endDiv = null;
    var range = this.getRange_();
    var dh = this.getFieldDomHelper();
    if (command in
        goog.editor.plugins.BasicTextFormatter.blockquoteHatingCommandsIE_) {
      var parent = range && range.getContainerElement();
      if (parent) {
        var blockquotes = goog.dom.getElementsByTagNameAndClass(
            goog.dom.TagName.BLOCKQUOTE, null, parent);
  
        // If a blockquote contains the selection, the fix is easy:
        // add a dummy div to the blockquote that isn't in the current selection.
        //
        // if the selection contains a blockquote,
        // there appears to be no easy way to protect it from getting mangled.
        // For now, we're just going to punt on this and try to
        // adjust the selection so that IE does something reasonable.
        //
        // TODO: Find a better fix for this.
        var bq;
        for (var i = 0; i < blockquotes.length; i++) {
          if (range.containsNode(blockquotes[i])) {
            bq = blockquotes[i];
            break;
          }
        }
  
        var bqThatNeedsDummyDiv =
            bq || goog.dom.getAncestorByTagNameAndClass(parent, 'BLOCKQUOTE');
        if (bqThatNeedsDummyDiv) {
          endDiv = dh.createDom('div', {style: 'height:0'});
          goog.dom.appendChild(bqThatNeedsDummyDiv, endDiv);
          toRemove.push(endDiv);
  
          if (bq) {
            range = goog.dom.Range.createFromNodes(bq, 0, endDiv, 0);
          } else if (range.containsNode(endDiv)) {
            // the selection might be the entire blockquote, and
            // it's important that endDiv not be in the selection.
            range = goog.dom.Range.createFromNodes(
                range.getStartNode(), range.getStartOffset(),
                endDiv, 0);
          }
          range.select();
        }
      }
    }
  
    // IE has a crazy bug where certain block execCommands cause it to mess with
    // the DOM nodes above the contentEditable element if the selection contains
    // or partially contains the last block element in the contentEditable
    // element.
    // Known commands: Indent, outdent, insertorderedlist, insertunorderedlist,
    // Justify (all of them)
  
    // Both of the above are "solved" by appending a dummy div to the field
    // before the execCommand and removing it after, but we don't need to do this
    // if we've alread added a dummy div somewhere else.
    var fieldObject = this.fieldObject;
    if (!fieldObject.usesIframe() && !endDiv) {
      if (command in
          goog.editor.plugins.BasicTextFormatter.brokenExecCommandsIE_) {
        var field = fieldObject.getElement();
  
        // If the field is totally empty, or if the field contains only text nodes
        // and the cursor is at the end of the field, then IE stills walks outside
        // the contentEditable region and destroys things AND justify will not
        // work. This is "solved" by adding a text node into the end of the
        // field and moving the cursor before it.
        if (range && range.isCollapsed() &&
            !goog.dom.getFirstElementChild(field)) {
          // The problem only occurs if the selection is at the end of the field.
          var selection = range.getTextRange(0).getBrowserRangeObject();
          var testRange = selection.duplicate();
          testRange.moveToElementText(field);
          testRange.collapse(false);
  
          if (testRange.isEqual(selection)) {
            // For reasons I really don't understand, if you use a breaking space
            // here, either " " or String.fromCharCode(32), this textNode becomes
            // corrupted, only after you hit ENTER to split it.  It exists in the
            // dom in that its parent has it as childNode and the parent's
            // innerText is correct, but the node itself throws invalid argument
            // errors when you try to access its data, parentNode, nextSibling,
            // previousSibling or most other properties.  WTF.
            var nbsp = dh.createTextNode(goog.string.Unicode.NBSP);
            field.appendChild(nbsp);
            selection.move('character', 1);
            selection.move('character', -1);
            selection.select();
            toRemove.push(nbsp);
          }
        }
  
        endDiv = dh.createDom('div', {style: 'height:0'});
        goog.dom.appendChild(field, endDiv);
        toRemove.push(endDiv);
      }
    }
  
    return toRemove;
  };
  
  
  /**
   * Fix a ridiculous Safari bug: the first letters of new headings
   * somehow retain their original font size and weight if multiple lines are
   * selected during the execCommand that turns them into headings.
   * The solution is to strip these styles which are normally stripped when
   * making things headings anyway.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.cleanUpSafariHeadings_ =
      function() {
    goog.iter.forEach(this.getRange_(), function(node) {
      if (node.className == 'Apple-style-span') {
        // These shouldn't persist after creating headings via
        // a FormatBlock execCommand.
        node.style.fontSize = '';
        node.style.fontWeight = '';
      }
    });
  };
  
  
  /**
   * Prevent Safari from making each list item be "1" when converting from
   * unordered to ordered lists.
   * (see https://bugs.webkit.org/show_bug.cgi?id=19539 )
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.fixSafariLists_ = function() {
    var previousList = false;
    goog.iter.forEach(this.getRange_(), function(node) {
      var tagName = node.tagName;
      if (tagName == goog.dom.TagName.UL || tagName == goog.dom.TagName.OL) {
        // Don't disturb lists outside of the selection. If this is the first <ul>
        // or <ol> in the range, we don't really want to merge the previous list
        // into it, since that list isn't in the range.
        if (!previousList) {
          previousList = true;
          return;
        }
        // The lists must be siblings to be merged; otherwise, indented sublists
        // could be broken.
        var previousElementSibling = goog.dom.getPreviousElementSibling(node);
        if (!previousElementSibling) {
          return;
        }
        // Make sure there isn't text between the two lists before they are merged
        var range = node.ownerDocument.createRange();
        range.setStartAfter(previousElementSibling);
        range.setEndBefore(node);
        if (!goog.string.isEmpty(range.toString())) {
          return;
        }
        // Make sure both are lists of the same type (ordered or unordered)
        if (previousElementSibling.nodeName == node.nodeName) {
          // We must merge the previous list into this one. Moving around
          // the current node will break the iterator, so we can't merge
          // this list into the previous one.
          while (previousElementSibling.lastChild) {
            node.insertBefore(previousElementSibling.lastChild, node.firstChild);
          }
          previousElementSibling.parentNode.removeChild(previousElementSibling);
        }
      }
    });
  };
  
  
  /**
   * Sane "type" attribute values for OL elements
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.orderedListTypes_ = {
    '1' : 1,
    'a' : 1,
    'A' : 1,
    'i' : 1,
    'I' : 1
  };
  
  
  /**
   * Sane "type" attribute values for UL elements
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.unorderedListTypes_ = {
    'disc' : 1,
    'circle' : 1,
    'square' : 1
  };
  
  
  /**
   * Changing an OL to a UL (or the other way around) will fail if the list
   * has a type attribute (such as "UL type=disc" becoming "OL type=disc", which
   * is visually identical). Most browsers will remove the type attribute
   * automatically, but IE doesn't. This does it manually.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.fixIELists_ = function() {
    // Find the lowest-level <ul> or <ol> that contains the entire range.
    var range = this.getRange_();
    var container = range && range.getContainer();
    while (container &&
           container.tagName != goog.dom.TagName.UL &&
           container.tagName != goog.dom.TagName.OL) {
      container = container.parentNode;
    }
    if (container) {
      // We want the parent node of the list so that we can grab it using
      // getElementsByTagName
      container = container.parentNode;
    }
    if (!container) return;
    var lists = goog.array.toArray(
        container.getElementsByTagName(goog.dom.TagName.UL));
    goog.array.extend(lists, goog.array.toArray(
        container.getElementsByTagName(goog.dom.TagName.OL)));
    // Fix the lists
    goog.array.forEach(lists, function(node) {
      var type = node.type;
      if (type) {
        var saneTypes =
            (node.tagName == goog.dom.TagName.UL ?
             goog.editor.plugins.BasicTextFormatter.unorderedListTypes_ :
             goog.editor.plugins.BasicTextFormatter.orderedListTypes_);
        if (!saneTypes[type]) {
          node.type = '';
        }
      }
    });
  };
  
  
  /**
   * In WebKit, the following commands will modify the node with
   * contentEditable=true if there are no block-level elements.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.brokenExecCommandsSafari_ = {
    'justifyCenter' : 1,
    'justifyFull' : 1,
    'justifyRight': 1,
    'justifyLeft': 1,
    'formatBlock' : 1
  };
  
  
  /**
   * In WebKit, the following commands can hang the browser if the selection
   * touches the beginning of the field.
   * https://bugs.webkit.org/show_bug.cgi?id=19735
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.hangingExecCommandWebkit_ = {
    'insertOrderedList': 1,
    'insertUnorderedList': 1
  };
  
  
  /**
   * Apply pre-execCommand fixes for Safari.
   * @param {string} command The command to execute.
   * @return {Element|undefined} The div added to the field.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.applyExecCommandSafariFixes_ =
      function(command) {
    // See the comment on brokenExecCommandsSafari_
    var div;
    if (goog.editor.plugins.BasicTextFormatter.brokenExecCommandsSafari_[
            command]) {
      // Add a new div at the end of the field.
      // Safari knows that it would be wrong to apply text-align to the
      // contentEditable element if there are non-empty block nodes in the field,
      // because then it would align them too. So in this case, it will
      // enclose the current selection in a block node.
      div = this.getFieldDomHelper().createDom(
          'div', {'style': 'height: 0'}, 'x');
      goog.dom.appendChild(this.fieldObject.getElement(), div);
    }
  
    if (goog.editor.plugins.BasicTextFormatter.hangingExecCommandWebkit_[
            command]) {
      // Add a new div at the beginning of the field.
      var field = this.fieldObject.getElement();
      div = this.getFieldDomHelper().createDom(
          'div', {'style': 'height: 0'}, 'x');
      field.insertBefore(div, field.firstChild);
    }
  
    return div;
  };
  
  
  /**
   * Apply pre-execCommand fixes for Gecko.
   * @param {string} command The command to execute.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.applyExecCommandGeckoFixes_ =
      function(command) {
    if (goog.userAgent.isVersion('1.9') &&
        command.toLowerCase() == 'formatblock') {
      // Firefox 3 and above throw a JS error for formatblock if the range is
      // a child of the body node. Changing the selection to the BR fixes the
      // problem.
      // See https://bugzilla.mozilla.org/show_bug.cgi?id=481696
      var range = this.getRange_();
      var startNode = range.getStartNode();
      if (range.isCollapsed() && startNode &&
          startNode.tagName == goog.dom.TagName.BODY) {
        var startOffset = range.getStartOffset();
        var childNode = startNode.childNodes[startOffset];
        if (childNode && childNode.tagName == goog.dom.TagName.BR) {
          // Change the range using getBrowserRange() because goog.dom.TextRange
          // will avoid setting <br>s directly.
          // @see goog.dom.TextRange#createFromNodes
          var browserRange = range.getBrowserRangeObject();
          browserRange.setStart(childNode, 0);
          browserRange.setEnd(childNode, 0);
        }
      }
    }
  };
  
  
  /**
   * Workaround for Opera bug CORE-23903. Opera sometimes fails to invalidate
   * serialized CSS or innerHTML for the DOM after certain execCommands when
   * styleWithCSS is on. Toggling an inline style on the elements fixes it.
   * TODO: Opera says they're going to prioritize a fix for this. Remove
   *              this code once that fix is released.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.invalidateInlineCss_ =
      function() {
    var ancestors = [];
    var ancestor = this.fieldObject.getRange().getContainerElement();
    do {
      ancestors.push(ancestor);
    } while (ancestor = ancestor.parentNode);
    var nodesInSelection = goog.iter.chain(
        goog.iter.toIterator(this.fieldObject.getRange()),
        goog.iter.toIterator(ancestors));
    var containersInSelection =
        goog.iter.filter(nodesInSelection, goog.editor.style.isContainer);
    goog.iter.forEach(containersInSelection, function(element) {
      var oldOutline = element.style.outline;
      element.style.outline = '0px solid red';
      element.style.outline = oldOutline;
    });
  };
  
  
  /**
   * Work around a Gecko bug that causes inserted lists to forget the current
   * font. This affects WebKit in the same way and Opera in a slightly different
   * way, but this workaround only works in Gecko.
   * WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=19653
   * Mozilla bug: https://bugzilla.mozilla.org/show_bug.cgi?id=439966
   * Opera bug: https://bugs.opera.com/show_bug.cgi?id=340392
   * TODO: work around this issue in WebKit and Opera as well.
   * @return {boolean} Whether the workaround was applied.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.beforeInsertListGecko_ =
      function() {
    var tag = this.fieldObject.queryCommandValue(goog.editor.Command.DEFAULT_TAG);
    if (tag == goog.dom.TagName.P || tag == goog.dom.TagName.DIV) {
      return false;
    }
  
    // Prevent Firefox from forgetting current formatting
    // when creating a list.
    // The bug happens with a collapsed selection, but it won't
    // happen when text with the desired formatting is selected.
    // So, we insert some dummy text, insert the list,
    // then remove the dummy text (while preserving its formatting).
    // (This formatting bug also affects WebKit, but this fix
    // only seems to work in Firefox)
    var range = this.getRange_();
    if (range.isCollapsed() &&
       (range.getContainer().nodeType != goog.dom.NodeType.TEXT)) {
      var tempTextNode = this.getFieldDomHelper().
          createTextNode(goog.string.Unicode.NBSP);
      range.insertNode(tempTextNode, false);
      goog.dom.Range.createFromNodeContents(tempTextNode).select();
      return true;
    }
    return false;
  };
  
  
  // Helpers for queryCommandState
  
  /**
   * Hash of suppoted justifications.
   * @type {Object}
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.SUPPORTED_JUSTIFICATIONS_ = {
    'center': 1,
    'justify': 1,
    'right': 1,
    'left': 1
  };
  
  /**
   * Returns true if the current justification matches the justification
   * command for the entire selection.
   * @param {string} command The justification command to check for.
   * @return {boolean} Whether the current justification matches the justification
   *     command for the entire selection.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.isJustification_ =
      function(command) {
    var alignment = command.replace('+justify', '').toLowerCase();
    var bidiPlugin = this.fieldObject.getPluginByClassId('Bidi');
    if (bidiPlugin) {
      // BiDi aware version
  
      // TODO: Since getComputedStyle is not used here, this version may be even
      // faster. If profiling confirms that it would be good to use this approach
      // in both cases. Otherwise the bidi part should be moved into an
      // execCommand so this bidi plugin dependence isn't needed here.
      /** @type {Function} */
      bidiPlugin.getSelectionAlignment;
      return alignment == bidiPlugin.getSelectionAlignment();
    } else {
      // BiDi unaware version
      var range = this.getRange_();
      if (!range) {
        // When nothing is in the selection then no justification
        // command matches.
        return false;
      }
  
      var parent = range.getContainerElement();
      var nodes =
          goog.array.filter(
              parent.childNodes,
              function(node) {
                return goog.editor.node.isImportant(node) &&
                    range.containsNode(node, true);
              });
      nodes = nodes.length ? nodes : [parent];
  
      for (var i = 0; i < nodes.length; i++) {
        var current = nodes[i];
  
        // If any node in the selection is not aligned the way we are checking,
        // then the justification command does not match.
        var container = goog.editor.style.getContainer(
            /** @type {Node} */ (current));
        if (alignment !=
            goog.editor.plugins.BasicTextFormatter.getNodeJustification_(
                container)) {
          return false;
        }
      }
  
      // If all nodes in the selection are aligned the way we are checking,
      // the justification command does match.
      return true;
    }
  };
  
  
  /**
   * Determines the justification for a given block-level element.
   * @param {Element} element The node to get justification for.
   * @return {string} The justification for a given block-level node.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.getNodeJustification_ =
      function(element) {
    var value = goog.style.getComputedTextAlign(element);
  
    // If there is no alignment, try the inline property,
    // otherwise assume left aligned.
    // TODO: for rtl languages we probably need to assume right.
    if (!goog.editor.plugins.BasicTextFormatter.SUPPORTED_JUSTIFICATIONS_[
            value]) {
      value = element.align || 'left';
    }
    return /** @type {string} */ (value);
  };
  
  
  /**
   * Returns true if a selection contained in the node should set the appropriate
   * toolbar state for the given nodeName, e.g. if the node is contained in a
   * strong element and nodeName is "strong", then it will return true.
   * @param {string} nodeName The type of node to check for.
   * @return {boolean} Whether the user's selection is in the given state.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.isNodeInState_ =
      function(nodeName) {
    var range = this.getRange_();
    var node = range && range.getContainerElement();
    var ancestor = goog.dom.getAncestorByTagNameAndClass(node, nodeName);
    return !!ancestor && goog.editor.node.isEditable(ancestor);
  };
  
  
  /**
   * Wrapper for browser's queryCommandState.
   * @param {Document|TextRange|Range} queryObject The object to query.
   * @param {string} command The command to check.
   * @param {boolean} opt_styleWithCss Set to true to enable styleWithCSS before
   *     performing the queryCommandState.
   * @return {boolean} The command state.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.queryCommandStateInternal_ =
      function(queryObject, command, opt_styleWithCss) {
    return /** @type {boolean} */ (this.queryCommandHelper_(true, queryObject,
        command, opt_styleWithCss));
  };
  
  
  /**
   * Wrapper for browser's queryCommandValue.
   * @param {Document|TextRange|Range} queryObject The object to query.
   * @param {string} command The command to check.
   * @param {boolean} opt_styleWithCss Set to true to enable styleWithCSS before
   *     performing the queryCommandValue.
   * @return {string|boolean|null} The command value.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.queryCommandValueInternal_ =
      function(queryObject, command, opt_styleWithCss) {
    return this.queryCommandHelper_(false, queryObject,
        command, opt_styleWithCss);
  };
  
  
  /**
   * Helper function to perform queryCommand(Value|State).
   * @param {boolean} isGetQueryCommandState True to use queryCommandState, false
   *     to use queryCommandValue.
   * @param {Document|TextRange|Range} queryObject The object to query.
   * @param {string} command The command to check.
   * @param {boolean} opt_styleWithCss Set to true to enable styleWithCSS before
   *     performing the queryCommand(Value|State).
   * @return {string|boolean|null} The command value.
   * @private
   */
  goog.editor.plugins.BasicTextFormatter.prototype.queryCommandHelper_ = function(
      isGetQueryCommandState, queryObject, command, opt_styleWithCss) {
    command =
        goog.editor.plugins.BasicTextFormatter.convertToRealExecCommand_(
            command);
    if (opt_styleWithCss) {
      var doc = this.getDocument_();
      // Don't use this.execCommandHelper_ here, as it is more heavyweight
      // and inserts a dummy div to protect against comamnds that could step
      // outside the editable region, which would cause change event on
      // every toolbar update.
      doc.execCommand('styleWithCSS', false, true);
    }
    var ret = isGetQueryCommandState ? queryObject.queryCommandState(command) :
        queryObject.queryCommandValue(command);
    if (opt_styleWithCss) {
      doc.execCommand('styleWithCSS', false, false);
    }
    return ret;
  };
  