// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Functions to style text.
 *
 * @author nicksantos@google.com (Nick Santos)
 */

goog.provide('goog.editor.plugins.BasicTextFormatter');
goog.provide('goog.editor.plugins.BasicTextFormatter.COMMAND');

goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.Range');
goog.require('goog.dom.TagName');
goog.require('goog.editor.BrowserFeature');
goog.require('goog.editor.Command');
goog.require('goog.editor.Link');
goog.require('goog.editor.Plugin');
goog.require('goog.editor.node');
goog.require('goog.editor.range');
goog.require('goog.editor.style');
goog.require('goog.iter');
goog.require('goog.iter.StopIteration');
goog.require('goog.log');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.string.Unicode');
goog.require('goog.style');
goog.require('goog.ui.editor.messages');
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);


/** @override */
goog.editor.plugins.BasicTextFormatter.prototype.getTrogClassId = function() {
  return 'BTF';
};


/**
 * Logging object.
 * @type {goog.log.Logger}
 * @protected
 * @override
 */
goog.editor.plugins.BasicTextFormatter.prototype.logger =
    goog.log.getLogger('goog.editor.plugins.BasicTextFormatter');


/**
 * Commands implemented by this plugin.
 * @enum {string}
 */
goog.editor.plugins.BasicTextFormatter.COMMAND = {
  LINK: '+link',
  FORMAT_BLOCK: '+formatBlock',
  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.
 * @override
 */
goog.editor.plugins.BasicTextFormatter.prototype.isSupportedCommand = function(
    command) {
  // TODO(user): 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.getFieldObject().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 {string} command Command to execute.
 * @param {...*} var_args For color commands, this
 *     should be the hex color (with the #). For FORMAT_BLOCK, this should be
 *     the goog.editor.plugins.BasicTextFormatter.BLOCK_COMMAND.
 *     It will be unused for other commands.
 * @return {Object|undefined} The result of the command.
 * @override
 */
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.LINK:
      result = this.toggleLink_(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 (goog.userAgent.IE &&
          command ==
              goog.editor.plugins.BasicTextFormatter.COMMAND.FORMAT_BLOCK &&
          opt_arg) {
        // IE requires that the argument be in the form of an opening
        // tag, like <h1>, including angle brackets.  WebKit will accept
        // the arguemnt with or without brackets, and Firefox pre-3 supports
        // only a fixed subset of tags with brackets, and prefers without.
        // So we only add them IE only.
        opt_arg = '<' + opt_arg + '>';
      }

      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.getFieldObject().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_();
          }
          // Fall through to preserveDir block

        case goog.editor.plugins.BasicTextFormatter.COMMAND.FORMAT_BLOCK:
          // Both FF & IE may lose directionality info. Save/restore it.
          // TODO(user): 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.getFieldObject().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(user): What about WebKit and Opera?
          styleWithCss = goog.userAgent.GECKO &&
                         goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS &&
                         this.queryCommandValue(command);
          break;

        case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_COLOR:
        case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_FACE:
          // 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.
          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.getFieldObject().inModalMode()) {
    this.focusField_();
  }
  return result;
};


/**
 * Focuses on the field.
 * @private
 */
goog.editor.plugins.BasicTextFormatter.prototype.focusField_ = function() {
  this.getFieldDomHelper().getWindow().focus();
};


/**
 * 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.
 * @override
 */
goog.editor.plugins.BasicTextFormatter.prototype.queryCommandValue = function(
    command) {
  var styleWithCss;
  switch (command) {
    case goog.editor.plugins.BasicTextFormatter.COMMAND.LINK:
      return this.isNodeInState_(goog.dom.TagName.A);

    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.FORMAT_BLOCK:
      // TODO(nicksantos): See if we can use queryCommandValue here.
      return goog.editor.plugins.BasicTextFormatter.getSelectionBlockState_(
          this.getFieldObject().getRange());

    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);
  }
};


/**
 * @override
 */
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;
};


/**
 * @override
 */
goog.editor.plugins.BasicTextFormatter.prototype.cleanContentsDom =
    function(fieldCopy) {
  var images = fieldCopy.getElementsByTagName(goog.dom.TagName.IMG);
  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.removeUid(image);

      // Declare oldTypeIndex for the compiler. The associated plugin may not be
      // included in the compiled bundle.
      /** @type {string} */ image.oldTabIndex;

      // oldTabIndex will only be set if
      // goog.editor.BrowserFeature.TABS_THROUGH_IMAGES is true and we're in
      // P-on-enter mode.
      if (image.oldTabIndex) {
        image.tabIndex = image.oldTabIndex;
      }
    }
  }
};


/**
 * @override
 */
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.getFieldObject().getEditableDomHelper().
        getElementsByTagNameAndClass(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 = heads[i].getElementsByTagName(goog.dom.TagName.STYLE);
      var numStyles = styles.length;
      for (var j = 0; j < numStyles; ++j) {
        stylesHtmlArr.push(styles[j].outerHTML);
      }
    }
    return stylesHtmlArr.join('') + html;
  }

  return html;
};


/**
 * @override
 */
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;
    case 's': // Ctrl+S
      // TODO(user): This doesn't belong in here.  Clients should handle
      // this themselves.
      // Catching control + s prevents the annoying browser save dialog
      // from appearing.
      return true;
  }

  if (command) {
    this.getFieldObject().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(user): 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';
      var newHtml = parent.innerHTML.replace(
          goog.editor.plugins.BasicTextFormatter.BR_REGEXP_,
          '<p$1 ' + attribute + '="' + value + '">');
      goog.editor.node.replaceInnerHtml(parent, newHtml);

      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(user): 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(user): What about WebKit or Opera?
  if (!(goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS &&
        goog.userAgent.GECKO)) {
    goog.iter.forEach(this.getFieldObject().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(user): 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.getFieldObject().queryCommandValue(
            goog.editor.Command.DIR_RTL) ? 'rtl' :
        this.getFieldObject().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.isVersionOrHigher('526') &&
      command.toLowerCase() == 'formatblock' &&
      opt_value && /^[<]?h\d[>]?$/i.test(opt_value)) {
    this.cleanUpSafariHeadings_();
  }

  if (/insert(un)?orderedlist/i.test(command)) {
    // NOTE(user): 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 &&
        !goog.userAgent.isVersionOrHigher(534)) {
      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.getFieldObject().execCommand(dir);
  }
};


/**
 * Applies a background color to a selection when the browser can't do the job.
 *
 * NOTE(nicksantos): 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.getFieldObject().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.
  }
};


/**
 * Toggle link for the current selection:
 *   If selection contains a link, unlink it, return null.
 *   Otherwise, make selection into a link, return the link.
 * @param {string=} opt_target Target for the link.
 * @return {goog.editor.Link?} The resulting link, or null if a link was
 *     removed.
 * @private
 */
goog.editor.plugins.BasicTextFormatter.prototype.toggleLink_ = function(
    opt_target) {
  if (!this.getFieldObject().isSelectionEditable()) {
    this.focusField_();
  }

  var range = this.getRange_();
  // Since we wrap images in links, its possible that the user selected an
  // image and clicked link, in which case we want to actually use the
  // image as the selection.
  var parent = range && range.getContainerElement();
  var link = /** @type {Element} */ (
      goog.dom.getAncestorByTagNameAndClass(parent, goog.dom.TagName.A));
  if (link && goog.editor.node.isEditable(link)) {
    goog.dom.flattenElement(link);
  } else {
    var editableLink = this.createLink_(range, '/', opt_target);
    if (editableLink) {
      if (!this.getFieldObject().execCommand(
          goog.editor.Command.MODAL_LINK_EDITOR, editableLink)) {
        var url = this.getFieldObject().getAppWindow().prompt(
            goog.ui.editor.messages.MSG_LINK_TO, 'http://');
        if (url) {
          editableLink.setTextAndUrl(editableLink.getCurrentText() || url, url);
          editableLink.placeCursorRightOf();
        } else {
          var savedRange = goog.editor.range.saveUsingNormalizedCarets(
              goog.dom.Range.createFromNodeContents(editableLink.getAnchor()));
          editableLink.removeLink();
          savedRange.restore().select();
          return null;
        }
      }
      return editableLink;
    }
  }
  return null;
};


/**
 * Create a link out of the current selection.  If nothing is selected, insert
 * a new link.  Otherwise, enclose the selection in a link.
 * @param {goog.dom.AbstractRange} range The closure range object for the
 *     current selection.
 * @param {string} url The url to link to.
 * @param {string=} opt_target Target for the link.
 * @return {goog.editor.Link?} The newly created link, or null if the link
 *     couldn't be created.
 * @private
 */
goog.editor.plugins.BasicTextFormatter.prototype.createLink_ = function(range,
    url, opt_target) {
  var anchor = null;
  var anchors = [];
  var parent = range && range.getContainerElement();
  // We do not yet support creating links around images.  Instead of throwing
  // lots of js errors, just fail silently.
  // TODO(user): Add support for linking images.
  if (parent && parent.tagName == goog.dom.TagName.IMG) {
    return null;
  }
  // If range is not present, the editable field doesn't have focus, abort
  // creating a link.
  if (!range) {
    return null;
  }

  if (range.isCollapsed()) {
    var textRange = range.getTextRange(0).getBrowserRangeObject();
    if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
      anchor = this.getFieldDomHelper().createElement(goog.dom.TagName.A);
      textRange.insertNode(anchor);
    } else if (goog.editor.BrowserFeature.HAS_IE_RANGES) {
      // TODO: Use goog.dom.AbstractRange's surroundContents
      textRange.pasteHTML("<a id='newLink'></a>");
      anchor = this.getFieldDomHelper().getElement('newLink');
      anchor.removeAttribute('id');
    }
  } else {
    // Create a unique identifier for the link so we can retrieve it later.
    // execCommand doesn't return the link to us, and we need a way to find
    // the newly created link in the dom, and the url is the only property
    // we have control over, so we set that to be unique and then find it.
    var uniqueId = goog.string.createUniqueString();
    this.execCommandHelper_('CreateLink', uniqueId);
    var setHrefAndLink = function(element, index, arr) {
      // We can't do straight comparision since the href can contain the
      // absolute url.
      if (goog.string.endsWith(element.href, uniqueId)) {
        anchors.push(element);
      }
    };

    goog.array.forEach(this.getFieldObject().getElement().getElementsByTagName(
        goog.dom.TagName.A), setHrefAndLink);
    if (anchors.length) {
      anchor = anchors.pop();
    }
    var isLikelyUrl = function(a, i, anchors) {
      return goog.editor.Link.isLikelyUrl(goog.dom.getRawTextContent(a));
    };
    if (anchors.length && goog.array.every(anchors, isLikelyUrl)) {
      for (var i = 0, a; a = anchors[i]; i++) {
        goog.editor.Link.createNewLinkFromText(a, opt_target);
      }
      anchors = null;
    }
  }

  return goog.editor.Link.createNewLink(
      /** @type {HTMLAnchorElement} */ (anchor), url, opt_target, anchors);
};


//---------------------------------------------------------------------
// 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.getFieldObject().getRange(),
                                       this.getFieldObject().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(nicksantos): 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.getFieldObject();
  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, fixed by 2010-04-21)
 * @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.isEmptyOrWhitespace(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.getFieldObject().getElement(), div);
  }

  if (!goog.userAgent.isVersionOrHigher(534) &&
      goog.editor.plugins.BasicTextFormatter.
          hangingExecCommandWebkit_[command]) {
    // Add a new div at the beginning of the field.
    var field = this.getFieldObject().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.isVersionOrHigher('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.
 * @private
 */
goog.editor.plugins.BasicTextFormatter.prototype.invalidateInlineCss_ =
    function() {
  var ancestors = [];
  var ancestor = this.getFieldObject().getRange().getContainerElement();
  do {
    ancestors.push(ancestor);
  } while (ancestor = ancestor.parentNode);
  var nodesInSelection = goog.iter.chain(
      goog.iter.toIterator(this.getFieldObject().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.getFieldObject().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


/**
 * Get the toolbar state for the block-level elements in the given range.
 * @param {goog.dom.AbstractRange} range The range to get toolbar state for.
 * @return {string?} The selection block state.
 * @private
 */
goog.editor.plugins.BasicTextFormatter.getSelectionBlockState_ =
    function(range) {
  var tagName = null;
  goog.iter.forEach(range, function(node, ignore, it) {
    if (!it.isEndTag()) {
      // Iterate over all containers in the range, checking if they all have the
      // same tagName.
      var container = goog.editor.style.getContainer(node);
      var thisTagName = container.tagName;
      tagName = tagName || thisTagName;

      if (tagName != thisTagName) {
        // If we find a container tag that doesn't match, exit right away.
        tagName = null;
        throw goog.iter.StopIteration;
      }

      // Skip the tag.
      it.skipTag();
    }
  });

  return tagName;
};


/**
 * 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();
  if (alignment == 'full') {
    alignment = 'justify';
  }
  var bidiPlugin = this.getFieldObject().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);
  // Strip preceding -moz- or -webkit- (@bug 2472589).
  value = value.replace(/^-(moz|webkit)-/, '');

  // 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;
};
