| /**
 * InsertContent.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */
/**
 * Handles inserts of contents into the editor instance.
 *
 * @class tinymce.InsertContent
 * @private
 */
define(
  'tinymce.core.InsertContent',
  [
    'ephox.katamari.api.Option',
    'ephox.sugar.api.node.Element',
    'tinymce.core.Env',
    'tinymce.core.InsertList',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.caret.CaretWalker',
    'tinymce.core.dom.ElementUtils',
    'tinymce.core.dom.NodeType',
    'tinymce.core.dom.PaddingBr',
    'tinymce.core.html.Serializer',
    'tinymce.core.selection.RangeNormalizer',
    'tinymce.core.util.Tools'
  ],
  function (Option, Element, Env, InsertList, CaretPosition, CaretWalker, ElementUtils, NodeType, PaddingBr, Serializer, RangeNormalizer, Tools) {
    var isTableCell = NodeType.matchNodeNames('td th');
    var validInsertion = function (editor, value, parentNode) {
      // Should never insert content into bogus elements, since these can
      // be resize handles or similar
      if (parentNode.getAttribute('data-mce-bogus') === 'all') {
        parentNode.parentNode.insertBefore(editor.dom.createFragment(value), parentNode);
      } else {
        // Check if parent is empty or only has one BR element then set the innerHTML of that parent
        var node = parentNode.firstChild;
        var node2 = parentNode.lastChild;
        if (!node || (node === node2 && node.nodeName === 'BR')) {///
          editor.dom.setHTML(parentNode, value);
        } else {
          editor.selection.setContent(value);
        }
      }
    };
    var trimBrsFromTableCell = function (dom, elm) {
      Option.from(dom.getParent(elm, 'td,th')).map(Element.fromDom).each(PaddingBr.trimBlockTrailingBr);
    };
    var insertHtmlAtCaret = function (editor, value, details) {
      var parser, serializer, parentNode, rootNode, fragment, args;
      var marker, rng, node, node2, bookmarkHtml, merge;
      var textInlineElements = editor.schema.getTextInlineElements();
      var selection = editor.selection, dom = editor.dom;
      var trimOrPaddLeftRight = function (html) {
        var rng, container, offset;
        rng = selection.getRng(true);
        container = rng.startContainer;
        offset = rng.startOffset;
        var hasSiblingText = function (siblingName) {
          return container[siblingName] && container[siblingName].nodeType == 3;
        };
        if (container.nodeType == 3) {
          if (offset > 0) {
            html = html.replace(/^ /, ' ');
          } else if (!hasSiblingText('previousSibling')) {
            html = html.replace(/^ /, ' ');
          }
          if (offset < container.length) {
            html = html.replace(/ (<br>|)$/, ' ');
          } else if (!hasSiblingText('nextSibling')) {
            html = html.replace(/( | )(<br>|)$/, ' ');
          }
        }
        return html;
      };
      // Removes   from a [b] c -> a  c -> a c
      var trimNbspAfterDeleteAndPaddValue = function () {
        var rng, container, offset;
        rng = selection.getRng(true);
        container = rng.startContainer;
        offset = rng.startOffset;
        if (container.nodeType == 3 && rng.collapsed) {
          if (container.data[offset] === '\u00a0') {
            container.deleteData(offset, 1);
            if (!/[\u00a0| ]$/.test(value)) {
              value += ' ';
            }
          } else if (container.data[offset - 1] === '\u00a0') {
            container.deleteData(offset - 1, 1);
            if (!/[\u00a0| ]$/.test(value)) {
              value = ' ' + value;
            }
          }
        }
      };
      var reduceInlineTextElements = function () {
        if (merge) {
          var root = editor.getBody(), elementUtils = new ElementUtils(dom);
          Tools.each(dom.select('*[data-mce-fragment]'), function (node) {
            for (var testNode = node.parentNode; testNode && testNode != root; testNode = testNode.parentNode) {
              if (textInlineElements[node.nodeName.toLowerCase()] && elementUtils.compare(testNode, node)) {
                dom.remove(node, true);
              }
            }
          });
        }
      };
      var markFragmentElements = function (fragment) {
        var node = fragment;
        while ((node = node.walk())) {
          if (node.type === 1) {
            node.attr('data-mce-fragment', '1');
          }
        }
      };
      var umarkFragmentElements = function (elm) {
        Tools.each(elm.getElementsByTagName('*'), function (elm) {
          elm.removeAttribute('data-mce-fragment');
        });
      };
      var isPartOfFragment = function (node) {
        return !!node.getAttribute('data-mce-fragment');
      };
      var canHaveChildren = function (node) {
        return node && !editor.schema.getShortEndedElements()[node.nodeName];
      };
      var moveSelectionToMarker = function (marker) {
        var parentEditableFalseElm, parentBlock, nextRng;
        var getContentEditableFalseParent = function (node) {
          var root = editor.getBody();
          for (; node && node !== root; node = node.parentNode) {
            if (editor.dom.getContentEditable(node) === 'false') {
              return node;
            }
          }
          return null;
        };
        if (!marker) {
          return;
        }
        selection.scrollIntoView(marker);
        // If marker is in cE=false then move selection to that element instead
        parentEditableFalseElm = getContentEditableFalseParent(marker);
        if (parentEditableFalseElm) {
          dom.remove(marker);
          selection.select(parentEditableFalseElm);
          return;
        }
        // Move selection before marker and remove it
        rng = dom.createRng();
        // If previous sibling is a text node set the selection to the end of that node
        node = marker.previousSibling;
        if (node && node.nodeType == 3) {
          rng.setStart(node, node.nodeValue.length);
          // TODO: Why can't we normalize on IE
          if (!Env.ie) {
            node2 = marker.nextSibling;
            if (node2 && node2.nodeType == 3) {
              node.appendData(node2.data);
              node2.parentNode.removeChild(node2);
            }
          }
        } else {
          // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node
          rng.setStartBefore(marker);
          rng.setEndBefore(marker);
        }
        var findNextCaretRng = function (rng) {
          var caretPos = CaretPosition.fromRangeStart(rng);
          var caretWalker = new CaretWalker(editor.getBody());
          caretPos = caretWalker.next(caretPos);
          if (caretPos) {
            return caretPos.toRange();
          }
        };
        // Remove the marker node and set the new range
        parentBlock = dom.getParent(marker, dom.isBlock);
        dom.remove(marker);
        if (parentBlock && dom.isEmpty(parentBlock)) {
          editor.$(parentBlock).empty();
          rng.setStart(parentBlock, 0);
          rng.setEnd(parentBlock, 0);
          if (!isTableCell(parentBlock) && !isPartOfFragment(parentBlock) && (nextRng = findNextCaretRng(rng))) {
            rng = nextRng;
            dom.remove(parentBlock);
          } else {
            dom.add(parentBlock, dom.create('br', { 'data-mce-bogus': '1' }));
          }
        }
        selection.setRng(rng);
      };
      // Check for whitespace before/after value
      if (/^ | $/.test(value)) {
        value = trimOrPaddLeftRight(value);
      }
      // Setup parser and serializer
      parser = editor.parser;
      merge = details.merge;
      serializer = new Serializer({
        validate: editor.settings.validate
      }, editor.schema);
      bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark">​</span>';
      // Run beforeSetContent handlers on the HTML to be inserted
      args = { content: value, format: 'html', selection: true, paste: details.paste };
      args = editor.fire('BeforeSetContent', args);
      if (args.isDefaultPrevented()) {
        editor.fire('SetContent', { content: args.content, format: 'html', selection: true, paste: details.paste });
        return;
      }
      value = args.content;
      // Add caret at end of contents if it's missing
      if (value.indexOf('{$caret}') == -1) {
        value += '{$caret}';
      }
      // Replace the caret marker with a span bookmark element
      value = value.replace(/\{\$caret\}/, bookmarkHtml);
      // If selection is at <body>|<p></p> then move it into <body><p>|</p>
      rng = selection.getRng();
      var caretElement = rng.startContainer || (rng.parentElement ? rng.parentElement() : null);
      var body = editor.getBody();
      if (caretElement === body && selection.isCollapsed()) {
        if (dom.isBlock(body.firstChild) && canHaveChildren(body.firstChild) && dom.isEmpty(body.firstChild)) {
          rng = dom.createRng();
          rng.setStart(body.firstChild, 0);
          rng.setEnd(body.firstChild, 0);
          selection.setRng(rng);
        }
      }
      // Insert node maker where we will insert the new HTML and get it's parent
      if (!selection.isCollapsed()) {
        // Fix for #2595 seems that delete removes one extra character on
        // WebKit for some odd reason if you double click select a word
        editor.selection.setRng(RangeNormalizer.normalize(editor.selection.getRng()));
        editor.getDoc().execCommand('Delete', false, null);
        trimNbspAfterDeleteAndPaddValue();
      }
      parentNode = selection.getNode();
      // Parse the fragment within the context of the parent node
      var parserArgs = { context: parentNode.nodeName.toLowerCase(), data: details.data, insert: true };
      fragment = parser.parse(value, parserArgs);
      // Custom handling of lists
      if (details.paste === true && InsertList.isListFragment(editor.schema, fragment) && InsertList.isParentBlockLi(dom, parentNode)) {
        rng = InsertList.insertAtCaret(serializer, dom, editor.selection.getRng(true), fragment);
        editor.selection.setRng(rng);
        editor.fire('SetContent', args);
        return;
      }
      markFragmentElements(fragment);
      // Move the caret to a more suitable location
      node = fragment.lastChild;
      if (node.attr('id') == 'mce_marker') {
        marker = node;
        for (node = node.prev; node; node = node.walk(true)) {
          if (node.type == 3 || !dom.isBlock(node.name)) {
            if (editor.schema.isValidChild(node.parent.name, 'span')) {
              node.parent.insert(marker, node, node.name === 'br');
            }
            break;
          }
        }
      }
      editor._selectionOverrides.showBlockCaretContainer(parentNode);
      // If parser says valid we can insert the contents into that parent
      if (!parserArgs.invalid) {
        value = serializer.serialize(fragment);
        validInsertion(editor, value, parentNode);
      } else {
        // If the fragment was invalid within that context then we need
        // to parse and process the parent it's inserted into
        // Insert bookmark node and get the parent
        selection.setContent(bookmarkHtml);
        parentNode = selection.getNode();
        rootNode = editor.getBody();
        // Opera will return the document node when selection is in root
        if (parentNode.nodeType == 9) {
          parentNode = node = rootNode;
        } else {
          node = parentNode;
        }
        // Find the ancestor just before the root element
        while (node !== rootNode) {
          parentNode = node;
          node = node.parentNode;
        }
        // Get the outer/inner HTML depending on if we are in the root and parser and serialize that
        value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode);
        value = serializer.serialize(
          parser.parse(
            // Need to replace by using a function since $ in the contents would otherwise be a problem
            value.replace(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/i, function () {
              return serializer.serialize(fragment);
            })
          )
        );
        // Set the inner/outer HTML depending on if we are in the root or not
        if (parentNode == rootNode) {
          dom.setHTML(rootNode, value);
        } else {
          dom.setOuterHTML(parentNode, value);
        }
      }
      reduceInlineTextElements();
      moveSelectionToMarker(dom.get('mce_marker'));
      umarkFragmentElements(editor.getBody());
      trimBrsFromTableCell(editor.dom, editor.selection.getStart());
      editor.fire('SetContent', args);
      editor.addVisual();
    };
    var processValue = function (value) {
      var details;
      if (typeof value !== 'string') {
        details = Tools.extend({
          paste: value.paste,
          data: {
            paste: value.paste
          }
        }, value);
        return {
          content: value.content,
          details: details
        };
      }
      return {
        content: value,
        details: {}
      };
    };
    var insertAtCaret = function (editor, value) {
      var result = processValue(value);
      insertHtmlAtCaret(editor, result.content, result.details);
    };
    return {
      insertAtCaret: insertAtCaret
    };
  }
);
 |