// 4.7.1 (2017-10-09)
(function () {

var defs = {}; // id -> {dependencies, definition, instance (possibly undefined)}

// Used when there is no 'main' module.
// The name is probably (hopefully) unique so minification removes for releases.
var register_3795 = function (id) {
  var module = dem(id);
  var fragments = id.split('.');
  var target = Function('return this;')();
  for (var i = 0; i < fragments.length - 1; ++i) {
    if (target[fragments[i]] === undefined)
      target[fragments[i]] = {};
    target = target[fragments[i]];
  }
  target[fragments[fragments.length - 1]] = module;
};

var instantiate = function (id) {
  var actual = defs[id];
  var dependencies = actual.deps;
  var definition = actual.defn;
  var len = dependencies.length;
  var instances = new Array(len);
  for (var i = 0; i < len; ++i)
    instances[i] = dem(dependencies[i]);
  var defResult = definition.apply(null, instances);
  if (defResult === undefined)
     throw 'module [' + id + '] returned undefined';
  actual.instance = defResult;
};

var def = function (id, dependencies, definition) {
  if (typeof id !== 'string')
    throw 'module id must be a string';
  else if (dependencies === undefined)
    throw 'no dependencies for ' + id;
  else if (definition === undefined)
    throw 'no definition function for ' + id;
  defs[id] = {
    deps: dependencies,
    defn: definition,
    instance: undefined
  };
};

var dem = function (id) {
  var actual = defs[id];
  if (actual === undefined)
    throw 'module [' + id + '] was undefined';
  else if (actual.instance === undefined)
    instantiate(id);
  return actual.instance;
};

var req = function (ids, callback) {
  var len = ids.length;
  var instances = new Array(len);
  for (var i = 0; i < len; ++i)
    instances[i] = dem(ids[i]);
  callback.apply(null, instances);
};

var ephox = {};

ephox.bolt = {
  module: {
    api: {
      define: def,
      require: req,
      demand: dem
    }
  }
};

var define = def;
var require = req;
var demand = dem;
// this helps with minification when using a lot of global references
var defineGlobal = function (id, ref) {
  define(id, [], function () { return ref; });
};
/*jsc
["tinymce.core.api.Main","ephox.katamari.api.Fun","global!window","tinymce.core.api.Tinymce","global!Array","global!Error","tinymce.core.AddOnManager","tinymce.core.api.Formatter","tinymce.core.api.NotificationManager","tinymce.core.api.WindowManager","tinymce.core.dom.BookmarkManager","tinymce.core.dom.ControlSelection","tinymce.core.dom.DomQuery","tinymce.core.dom.DOMUtils","tinymce.core.dom.EventUtils","tinymce.core.dom.RangeUtils","tinymce.core.dom.ScriptLoader","tinymce.core.dom.Selection","tinymce.core.dom.Serializer","tinymce.core.dom.Sizzle","tinymce.core.dom.TreeWalker","tinymce.core.Editor","tinymce.core.EditorCommands","tinymce.core.EditorManager","tinymce.core.EditorObservable","tinymce.core.Env","tinymce.core.FocusManager","tinymce.core.geom.Rect","tinymce.core.html.DomParser","tinymce.core.html.Entities","tinymce.core.html.Node","tinymce.core.html.SaxParser","tinymce.core.html.Schema","tinymce.core.html.Serializer","tinymce.core.html.Styles","tinymce.core.html.Writer","tinymce.core.Shortcuts","tinymce.core.ui.Factory","tinymce.core.UndoManager","tinymce.core.util.Class","tinymce.core.util.Color","tinymce.core.util.Delay","tinymce.core.util.EventDispatcher","tinymce.core.util.I18n","tinymce.core.util.JSON","tinymce.core.util.JSONP","tinymce.core.util.JSONRequest","tinymce.core.util.LocalStorage","tinymce.core.util.Observable","tinymce.core.util.Promise","tinymce.core.util.Tools","tinymce.core.util.URI","tinymce.core.util.VK","tinymce.core.util.XHR","ephox.katamari.api.Arr","global!document","ephox.sand.api.URL","global!matchMedia","global!navigator","global!clearInterval","global!clearTimeout","global!setInterval","global!setTimeout","tinymce.core.util.Arr","tinymce.core.dom.StyleSheetLoader","ephox.sugar.api.node.Element","ephox.katamari.api.Cell","tinymce.core.fmt.ApplyFormat","tinymce.core.fmt.FormatChanged","tinymce.core.fmt.FormatRegistry","tinymce.core.fmt.MatchFormat","tinymce.core.fmt.Preview","tinymce.core.fmt.RemoveFormat","tinymce.core.fmt.ToggleFormat","tinymce.core.keyboard.FormatShortcuts","ephox.katamari.api.Option","tinymce.core.EditorView","tinymce.core.ui.NotificationManagerImpl","tinymce.core.selection.SelectionBookmark","tinymce.core.ui.WindowManagerImpl","tinymce.core.caret.CaretBookmark","tinymce.core.caret.CaretContainer","tinymce.core.caret.CaretPosition","tinymce.core.dom.NodeType","tinymce.core.text.Zwsp","ephox.sugar.api.search.Selectors","tinymce.core.dom.RangePoint","ephox.sugar.api.dom.Compare","tinymce.core.EditorFocus","tinymce.core.dom.ScrollIntoView","tinymce.core.selection.EventProcessRanges","tinymce.core.selection.GetSelectionContent","tinymce.core.selection.MultiRange","tinymce.core.selection.SetSelectionContent","tinymce.core.InsertContent","tinymce.core.delete.DeleteCommands","tinymce.core.EditorSettings","tinymce.core.init.Render","tinymce.core.Mode","tinymce.core.ui.Sidebar","tinymce.core.util.Uuid","ephox.katamari.api.Type","tinymce.core.ErrorReporter","tinymce.core.LegacyInput","tinymce.core.undo.Levels","ephox.sand.api.XMLHttpRequest","global!Object","global!String","ephox.sand.util.Global","ephox.katamari.api.Future","ephox.katamari.api.Futures","ephox.katamari.api.Result","global!console","tinymce.core.util.Fun","tinymce.core.caret.CaretCandidate","tinymce.core.geom.ClientRect","tinymce.core.text.ExtendingChar","tinymce.core.dom.RangeNormalizer","tinymce.core.fmt.CaretFormat","tinymce.core.fmt.ExpandRange","tinymce.core.fmt.FormatUtils","tinymce.core.fmt.Hooks","tinymce.core.fmt.MergeFormats","tinymce.core.fmt.DefaultFormats","ephox.sand.api.Node","ephox.sand.api.PlatformDetection","ephox.sugar.api.node.NodeTypes","ephox.sugar.api.properties.Css","ephox.sugar.api.search.Traverse","ephox.sugar.api.node.Node","ephox.sugar.api.node.Text","ephox.sugar.api.selection.Selection","ephox.sugar.api.selection.WindowSelection","tinymce.core.caret.CaretFinder","ephox.sugar.api.dom.Focus","tinymce.core.dom.ElementType","tinymce.core.selection.FragmentReader","tinymce.core.caret.CaretWalker","tinymce.core.dom.ElementUtils","tinymce.core.dom.PaddingBr","tinymce.core.InsertList","tinymce.core.delete.BlockBoundaryDelete","tinymce.core.delete.BlockRangeDelete","tinymce.core.delete.CefDelete","tinymce.core.delete.DeleteUtils","tinymce.core.delete.InlineBoundaryDelete","tinymce.core.delete.TableDelete","ephox.katamari.api.Obj","ephox.katamari.api.Strings","ephox.katamari.api.Struct","tinymce.core.init.Init","tinymce.core.PluginManager","tinymce.core.ThemeManager","tinymce.core.undo.Fragments","ephox.katamari.api.Resolve","ephox.katamari.api.LazyValue","ephox.katamari.async.Bounce","ephox.katamari.async.AsyncValues","tinymce.core.caret.CaretUtils","ephox.sugar.api.dom.Insert","ephox.sugar.api.dom.Remove","ephox.sugar.impl.NodeValue","ephox.sugar.api.search.SelectorFilter","ephox.katamari.data.Immutable","ephox.katamari.data.MixedBag","ephox.sugar.alien.Recurse","ephox.katamari.api.Thunk","ephox.sand.core.PlatformDetection","ephox.sugar.api.properties.Attr","ephox.sugar.api.node.Body","ephox.sugar.impl.Style","ephox.katamari.str.StrAppend","ephox.katamari.str.StringParts","ephox.katamari.api.Adt","ephox.sugar.api.dom.DocumentPosition","ephox.sugar.api.node.Fragment","ephox.sugar.api.selection.Situ","ephox.sugar.selection.core.NativeRange","ephox.sugar.selection.core.SelectionDirection","ephox.sugar.selection.query.CaretRange","ephox.sugar.selection.query.Within","ephox.sugar.selection.quirks.Prefilter","ephox.sugar.api.search.PredicateExists","ephox.sugar.api.dom.Replication","ephox.sugar.api.search.SelectorFind","tinymce.core.dom.Parents","tinymce.core.selection.SelectionUtils","tinymce.core.selection.SimpleTableModel","tinymce.core.selection.TableCellSelection","tinymce.core.delete.BlockBoundary","tinymce.core.delete.MergeBlocks","ephox.katamari.api.Options","ephox.sugar.api.search.PredicateFind","tinymce.core.delete.CefDeleteAction","tinymce.core.delete.DeleteElement","tinymce.core.keyboard.BoundaryCaret","tinymce.core.keyboard.BoundaryLocation","tinymce.core.keyboard.BoundarySelection","tinymce.core.keyboard.InlineUtils","tinymce.core.delete.TableDeleteAction","tinymce.core.dom.Empty","tinymce.core.init.InitContentBody","tinymce.core.undo.Diff","ephox.katamari.api.Global","ephox.katamari.util.BagUtils","ephox.sand.core.Browser","ephox.sand.core.OperatingSystem","ephox.sand.detect.DeviceType","ephox.sand.detect.UaString","ephox.sand.info.PlatformInfo","ephox.sugar.api.dom.InsertAll","ephox.sugar.api.search.PredicateFilter","ephox.sugar.selection.query.ContainerPoint","ephox.sugar.selection.query.EdgePoint","global!Math","ephox.sugar.impl.ClosestOrAncestor","ephox.sugar.api.search.SelectorExists","tinymce.core.caret.CaretContainerInline","tinymce.core.caret.CaretContainerRemove","tinymce.core.text.Bidi","tinymce.core.util.LazyEvaluator","tinymce.core.caret.CaretContainerInput","tinymce.core.EditorUpload","tinymce.core.ForceBlocks","tinymce.core.keyboard.KeyboardOverrides","tinymce.core.NodeChange","tinymce.core.SelectionOverrides","tinymce.core.util.Quirks","ephox.sand.detect.Version","ephox.sugar.selection.alien.Geometry","ephox.sugar.selection.query.TextPoint","ephox.sugar.api.selection.CursorPosition","tinymce.core.file.Uploader","tinymce.core.file.ImageScanner","tinymce.core.file.BlobCache","tinymce.core.file.UploadStatus","tinymce.core.keyboard.ArrowKeys","tinymce.core.keyboard.DeleteBackspaceKeys","tinymce.core.keyboard.EnterKey","tinymce.core.keyboard.SpaceKey","tinymce.core.DragDropOverrides","tinymce.core.caret.FakeCaret","tinymce.core.caret.LineUtils","tinymce.core.keyboard.CefUtils","tinymce.core.dom.NodePath","global!Number","ephox.sugar.api.selection.Awareness","tinymce.core.file.Conversions","tinymce.core.keyboard.CefNavigation","tinymce.core.keyboard.MatchKeys","tinymce.core.keyboard.InsertNewLine","tinymce.core.keyboard.InsertSpace","tinymce.core.dom.MousePosition","tinymce.core.dom.Dimensions","ephox.sand.api.Blob","ephox.sand.api.FileReader","ephox.sand.api.Uint8Array","ephox.sand.api.Window","tinymce.core.caret.LineWalker","ephox.katamari.api.Merger"]
jsc*/
defineGlobal("global!Array", Array);
defineGlobal("global!Error", Error);
define(
  'ephox.katamari.api.Fun',

  [
    'global!Array',
    'global!Error'
  ],

  function (Array, Error) {

    var noop = function () { };

    var compose = function (fa, fb) {
      return function () {
        return fa(fb.apply(null, arguments));
      };
    };

    var constant = function (value) {
      return function () {
        return value;
      };
    };

    var identity = function (x) {
      return x;
    };

    var tripleEquals = function(a, b) {
      return a === b;
    };

    // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome
    var curry = function (f) {
      // equivalent to arguments.slice(1)
      // starting at 1 because 0 is the f, makes things tricky.
      // Pay attention to what variable is where, and the -1 magic.
      // thankfully, we have tests for this.
      var args = new Array(arguments.length - 1);
      for (var i = 1; i < arguments.length; i++) args[i-1] = arguments[i];

      return function () {
        var newArgs = new Array(arguments.length);
        for (var j = 0; j < newArgs.length; j++) newArgs[j] = arguments[j];

        var all = args.concat(newArgs);
        return f.apply(null, all);
      };
    };

    var not = function (f) {
      return function () {
        return !f.apply(null, arguments);
      };
    };

    var die = function (msg) {
      return function () {
        throw new Error(msg);
      };
    };

    var apply = function (f) {
      return f();
    };

    var call = function(f) {
      f();
    };

    var never = constant(false);
    var always = constant(true);
    

    return {
      noop: noop,
      compose: compose,
      constant: constant,
      identity: identity,
      tripleEquals: tripleEquals,
      curry: curry,
      not: not,
      die: die,
      apply: apply,
      call: call,
      never: never,
      always: always
    };
  }
);

defineGlobal("global!window", window);
defineGlobal("global!Object", Object);
define(
  'ephox.katamari.api.Option',

  [
    'ephox.katamari.api.Fun',
    'global!Object'
  ],

  function (Fun, Object) {

    var never = Fun.never;
    var always = Fun.always;

    /**
      Option objects support the following methods:

      fold :: this Option a -> ((() -> b, a -> b)) -> Option b

      is :: this Option a -> a -> Boolean

      isSome :: this Option a -> () -> Boolean

      isNone :: this Option a -> () -> Boolean

      getOr :: this Option a -> a -> a

      getOrThunk :: this Option a -> (() -> a) -> a

      getOrDie :: this Option a -> String -> a

      or :: this Option a -> Option a -> Option a
        - if some: return self
        - if none: return opt

      orThunk :: this Option a -> (() -> Option a) -> Option a
        - Same as "or", but uses a thunk instead of a value

      map :: this Option a -> (a -> b) -> Option b
        - "fmap" operation on the Option Functor.
        - same as 'each'

      ap :: this Option a -> Option (a -> b) -> Option b
        - "apply" operation on the Option Apply/Applicative.
        - Equivalent to <*> in Haskell/PureScript.

      each :: this Option a -> (a -> b) -> Option b
        - same as 'map'

      bind :: this Option a -> (a -> Option b) -> Option b
        - "bind"/"flatMap" operation on the Option Bind/Monad.
        - Equivalent to >>= in Haskell/PureScript; flatMap in Scala.

      flatten :: {this Option (Option a))} -> () -> Option a
        - "flatten"/"join" operation on the Option Monad.

      exists :: this Option a -> (a -> Boolean) -> Boolean

      forall :: this Option a -> (a -> Boolean) -> Boolean

      filter :: this Option a -> (a -> Boolean) -> Option a

      equals :: this Option a -> Option a -> Boolean

      equals_ :: this Option a -> (Option a, a -> Boolean) -> Boolean

      toArray :: this Option a -> () -> [a]

    */

    var none = function () { return NONE; };

    var NONE = (function () {
      var eq = function (o) {
        return o.isNone();
      };

      // inlined from peanut, maybe a micro-optimisation?
      var call = function (thunk) { return thunk(); };
      var id = function (n) { return n; };
      var noop = function () { };

      var me = {
        fold: function (n, s) { return n(); },
        is: never,
        isSome: never,
        isNone: always,
        getOr: id,
        getOrThunk: call,
        getOrDie: function (msg) {
          throw new Error(msg || 'error: getOrDie called on none.');
        },
        or: id,
        orThunk: call,
        map: none,
        ap: none,
        each: noop,
        bind: none,
        flatten: none,
        exists: never,
        forall: always,
        filter: none,
        equals: eq,
        equals_: eq,
        toArray: function () { return []; },
        toString: Fun.constant("none()")
      };
      if (Object.freeze) Object.freeze(me);
      return me;
    })();


    /** some :: a -> Option a */
    var some = function (a) {

      // inlined from peanut, maybe a micro-optimisation?
      var constant_a = function () { return a; };

      var self = function () {
        // can't Fun.constant this one
        return me;
      };

      var map = function (f) {
        return some(f(a));
      };

      var bind = function (f) {
        return f(a);
      };

      var me = {
        fold: function (n, s) { return s(a); },
        is: function (v) { return a === v; },
        isSome: always,
        isNone: never,
        getOr: constant_a,
        getOrThunk: constant_a,
        getOrDie: constant_a,
        or: self,
        orThunk: self,
        map: map,
        ap: function (optfab) {
          return optfab.fold(none, function(fab) {
            return some(fab(a));
          });
        },
        each: function (f) {
          f(a);
        },
        bind: bind,
        flatten: constant_a,
        exists: bind,
        forall: bind,
        filter: function (f) {
          return f(a) ? me : NONE;
        },
        equals: function (o) {
          return o.is(a);
        },
        equals_: function (o, elementEq) {
          return o.fold(
            never,
            function (b) { return elementEq(a, b); }
          );
        },
        toArray: function () {
          return [a];
        },
        toString: function () {
          return 'some(' + a + ')';
        }
      };
      return me;
    };

    /** from :: undefined|null|a -> Option a */
    var from = function (value) {
      return value === null || value === undefined ? NONE : some(value);
    };

    return {
      some: some,
      none: none,
      from: from
    };
  }
);

defineGlobal("global!String", String);
define(
  'ephox.katamari.api.Arr',

  [
    'ephox.katamari.api.Option',
    'global!Array',
    'global!Error',
    'global!String'
  ],

  function (Option, Array, Error, String) {
    // Use the native Array.indexOf if it is available (IE9+) otherwise fall back to manual iteration
    // https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf
    var rawIndexOf = (function () {
      var pIndexOf = Array.prototype.indexOf;

      var fastIndex = function (xs, x) { return  pIndexOf.call(xs, x); };

      var slowIndex = function(xs, x) { return slowIndexOf(xs, x); };

      return pIndexOf === undefined ? slowIndex : fastIndex;
    })();

    var indexOf = function (xs, x) {
      // The rawIndexOf method does not wrap up in an option. This is for performance reasons.
      var r = rawIndexOf(xs, x);
      return r === -1 ? Option.none() : Option.some(r);
    };

    var contains = function (xs, x) {
      return rawIndexOf(xs, x) > -1;
    };

    // Using findIndex is likely less optimal in Chrome (dynamic return type instead of bool)
    // but if we need that micro-optimisation we can inline it later.
    var exists = function (xs, pred) {
      return findIndex(xs, pred).isSome();
    };

    var range = function (num, f) {
      var r = [];
      for (var i = 0; i < num; i++) {
        r.push(f(i));
      }
      return r;
    };

    // It's a total micro optimisation, but these do make some difference.
    // Particularly for browsers other than Chrome.
    // - length caching
    // http://jsperf.com/browser-diet-jquery-each-vs-for-loop/69
    // - not using push
    // http://jsperf.com/array-direct-assignment-vs-push/2

    var chunk = function (array, size) {
      var r = [];
      for (var i = 0; i < array.length; i += size) {
        var s = array.slice(i, i + size);
        r.push(s);
      }
      return r;
    };

    var map = function(xs, f) {
      // pre-allocating array size when it's guaranteed to be known
      // http://jsperf.com/push-allocated-vs-dynamic/22
      var len = xs.length;
      var r = new Array(len);
      for (var i = 0; i < len; i++) {
        var x = xs[i];
        r[i] = f(x, i, xs);
      }
      return r;
    };

    // Unwound implementing other functions in terms of each.
    // The code size is roughly the same, and it should allow for better optimisation.
    var each = function(xs, f) {
      for (var i = 0, len = xs.length; i < len; i++) {
        var x = xs[i];
        f(x, i, xs);
      }
    };

    var eachr = function (xs, f) {
      for (var i = xs.length - 1; i >= 0; i--) {
        var x = xs[i];
        f(x, i, xs);
      }
    };

    var partition = function(xs, pred) {
      var pass = [];
      var fail = [];
      for (var i = 0, len = xs.length; i < len; i++) {
        var x = xs[i];
        var arr = pred(x, i, xs) ? pass : fail;
        arr.push(x);
      }
      return { pass: pass, fail: fail };
    };

    var filter = function(xs, pred) {
      var r = [];
      for (var i = 0, len = xs.length; i < len; i++) {
        var x = xs[i];
        if (pred(x, i, xs)) {
          r.push(x);
        }
      }
      return r;
    };

    /*
     * Groups an array into contiguous arrays of like elements. Whether an element is like or not depends on f.
     *
     * f is a function that derives a value from an element - e.g. true or false, or a string.
     * Elements are like if this function generates the same value for them (according to ===).
     *
     *
     * Order of the elements is preserved. Arr.flatten() on the result will return the original list, as with Haskell groupBy function.
     *  For a good explanation, see the group function (which is a special case of groupBy)
     *  http://hackage.haskell.org/package/base-4.7.0.0/docs/Data-List.html#v:group
     */
    var groupBy = function (xs, f) {
      if (xs.length === 0) {
        return [];
      } else {
        var wasType = f(xs[0]); // initial case for matching
        var r = [];
        var group = [];

        for (var i = 0, len = xs.length; i < len; i++) {
          var x = xs[i];
          var type = f(x);
          if (type !== wasType) {
            r.push(group);
            group = [];
          }
          wasType = type;
          group.push(x);
        }
        if (group.length !== 0) {
          r.push(group);
        }
        return r;
      }
    };

    var foldr = function (xs, f, acc) {
      eachr(xs, function (x) {
        acc = f(acc, x);
      });
      return acc;
    };

    var foldl = function (xs, f, acc) {
      each(xs, function (x) {
        acc = f(acc, x);
      });
      return acc;
    };

    var find = function (xs, pred) {
      for (var i = 0, len = xs.length; i < len; i++) {
        var x = xs[i];
        if (pred(x, i, xs)) {
          return Option.some(x);
        }
      }
      return Option.none();
    };

    var findIndex = function (xs, pred) {
      for (var i = 0, len = xs.length; i < len; i++) {
        var x = xs[i];
        if (pred(x, i, xs)) {
          return Option.some(i);
        }
      }

      return Option.none();
    };

    var slowIndexOf = function (xs, x) {
      for (var i = 0, len = xs.length; i < len; ++i) {
        if (xs[i] === x) {
          return i;
        }
      }

      return -1;
    };

    var push = Array.prototype.push;
    var flatten = function (xs) {
      // Note, this is possible because push supports multiple arguments:
      // http://jsperf.com/concat-push/6
      // Note that in the past, concat() would silently work (very slowly) for array-like objects.
      // With this change it will throw an error.
      var r = [];
      for (var i = 0, len = xs.length; i < len; ++i) {
        // Ensure that each value is an array itself
        if (! Array.prototype.isPrototypeOf(xs[i])) throw new Error('Arr.flatten item ' + i + ' was not an array, input: ' + xs);
        push.apply(r, xs[i]);
      }
      return r;
    };

    var bind = function (xs, f) {
      var output = map(xs, f);
      return flatten(output);
    };

    var forall = function (xs, pred) {
      for (var i = 0, len = xs.length; i < len; ++i) {
        var x = xs[i];
        if (pred(x, i, xs) !== true) {
          return false;
        }
      }
      return true;
    };

    var equal = function (a1, a2) {
      return a1.length === a2.length && forall(a1, function (x, i) {
        return x === a2[i];
      });
    };

    var slice = Array.prototype.slice;
    var reverse = function (xs) {
      var r = slice.call(xs, 0);
      r.reverse();
      return r;
    };

    var difference = function (a1, a2) {
      return filter(a1, function (x) {
        return !contains(a2, x);
      });
    };

    var mapToObject = function(xs, f) {
      var r = {};
      for (var i = 0, len = xs.length; i < len; i++) {
        var x = xs[i];
        r[String(x)] = f(x, i);
      }
      return r;
    };

    var pure = function(x) {
      return [x];
    };

    var sort = function (xs, comparator) {
      var copy = slice.call(xs, 0);
      copy.sort(comparator);
      return copy;
    };

    var head = function (xs) {
      return xs.length === 0 ? Option.none() : Option.some(xs[0]);
    };

    var last = function (xs) {
      return xs.length === 0 ? Option.none() : Option.some(xs[xs.length - 1]);
    };

    return {
      map: map,
      each: each,
      eachr: eachr,
      partition: partition,
      filter: filter,
      groupBy: groupBy,
      indexOf: indexOf,
      foldr: foldr,
      foldl: foldl,
      find: find,
      findIndex: findIndex,
      flatten: flatten,
      bind: bind,
      forall: forall,
      exists: exists,
      contains: contains,
      equal: equal,
      reverse: reverse,
      chunk: chunk,
      difference: difference,
      mapToObject: mapToObject,
      pure: pure,
      sort: sort,
      range: range,
      head: head,
      last: last
    };
  }
);
defineGlobal("global!document", document);
define(
  'ephox.katamari.api.Global',

  [
  ],

  function () {
    // Use window object as the global if it's available since CSP will block script evals
    if (typeof window !== 'undefined') {
      return window;
    } else {
      return Function('return this;')();
    }
  }
);


define(
  'ephox.katamari.api.Resolve',

  [
    'ephox.katamari.api.Global'
  ],

  function (Global) {
    /** path :: ([String], JsObj?) -> JsObj */
    var path = function (parts, scope) {
      var o = scope !== undefined ? scope : Global;
      for (var i = 0; i < parts.length && o !== undefined && o !== null; ++i)
        o = o[parts[i]];
      return o;
    };

    /** resolve :: (String, JsObj?) -> JsObj */
    var resolve = function (p, scope) {
      var parts = p.split('.');
      return path(parts, scope);
    };

    /** step :: (JsObj, String) -> JsObj */
    var step = function (o, part) {
      if (o[part] === undefined || o[part] === null)
        o[part] = {};
      return o[part];
    };

    /** forge :: ([String], JsObj?) -> JsObj */
    var forge = function (parts, target) {
      var o = target !== undefined ? target : Global;      
      for (var i = 0; i < parts.length; ++i)
        o = step(o, parts[i]);
      return o;
    };

    /** namespace :: (String, JsObj?) -> JsObj */
    var namespace = function (name, target) {
      var parts = name.split('.');
      return forge(parts, target);
    };

    return {
      path: path,
      resolve: resolve,
      forge: forge,
      namespace: namespace
    };
  }
);


define(
  'ephox.sand.util.Global',

  [
    'ephox.katamari.api.Resolve'
  ],

  function (Resolve) {
    var unsafe = function (name, scope) {
      return Resolve.resolve(name, scope);
    };

    var getOrDie = function (name, scope) {
      var actual = unsafe(name, scope);

      if (actual === undefined) throw name + ' not available on this browser';
      return actual;
    };

    return {
      getOrDie: getOrDie
    };
  }
);
define(
  'ephox.sand.api.URL',

  [
    'ephox.sand.util.Global'
  ],

  function (Global) {
    /*
     * IE10 and above per
     * https://developer.mozilla.org/en-US/docs/Web/API/URL.createObjectURL
     *
     * Also Safari 6.1+
     * Safari 6.0 has 'webkitURL' instead, but doesn't support flexbox so we
     * aren't supporting it anyway
     */
    var url = function () {
      return Global.getOrDie('URL');
    };

    var createObjectURL = function (blob) {
      return url().createObjectURL(blob);
    };

    var revokeObjectURL = function (u) {
      url().revokeObjectURL(u);
    };

    return {
      createObjectURL: createObjectURL,
      revokeObjectURL: revokeObjectURL
    };
  }
);
defineGlobal("global!matchMedia", matchMedia);
defineGlobal("global!navigator", navigator);
/**
 * Env.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
 */

/**
 * This class contains various environment constants like browser versions etc.
 * Normally you don't want to sniff specific browser versions but sometimes you have
 * to when it's impossible to feature detect. So use this with care.
 *
 * @class tinymce.Env
 * @static
 */
define(
  'tinymce.core.Env',
  [
    'ephox.sand.api.URL',
    'global!document',
    'global!matchMedia',
    'global!navigator',
    'global!window'
  ],
  function (URL, document, matchMedia, navigator, window) {
    var nav = navigator, userAgent = nav.userAgent;
    var opera, webkit, ie, ie11, ie12, gecko, mac, iDevice, android, fileApi, phone, tablet, windowsPhone;

    var matchMediaQuery = function (query) {
      return "matchMedia" in window ? matchMedia(query).matches : false;
    };

    opera = window.opera && window.opera.buildNumber;
    android = /Android/.test(userAgent);
    webkit = /WebKit/.test(userAgent);
    ie = !webkit && !opera && (/MSIE/gi).test(userAgent) && (/Explorer/gi).test(nav.appName);
    ie = ie && /MSIE (\w+)\./.exec(userAgent)[1];
    ie11 = userAgent.indexOf('Trident/') != -1 && (userAgent.indexOf('rv:') != -1 || nav.appName.indexOf('Netscape') != -1) ? 11 : false;
    ie12 = (userAgent.indexOf('Edge/') != -1 && !ie && !ie11) ? 12 : false;
    ie = ie || ie11 || ie12;
    gecko = !webkit && !ie11 && /Gecko/.test(userAgent);
    mac = userAgent.indexOf('Mac') != -1;
    iDevice = /(iPad|iPhone)/.test(userAgent);
    fileApi = "FormData" in window && "FileReader" in window && "URL" in window && !!URL.createObjectURL;
    phone = matchMediaQuery("only screen and (max-device-width: 480px)") && (android || iDevice);
    tablet = matchMediaQuery("only screen and (min-width: 800px)") && (android || iDevice);
    windowsPhone = userAgent.indexOf('Windows Phone') != -1;

    if (ie12) {
      webkit = false;
    }

    // Is a iPad/iPhone and not on iOS5 sniff the WebKit version since older iOS WebKit versions
    // says it has contentEditable support but there is no visible caret.
    var contentEditable = !iDevice || fileApi || userAgent.match(/AppleWebKit\/(\d*)/)[1] >= 534;

    return {
      /**
       * Constant that is true if the browser is Opera.
       *
       * @property opera
       * @type Boolean
       * @final
       */
      opera: opera,

      /**
       * Constant that is true if the browser is WebKit (Safari/Chrome).
       *
       * @property webKit
       * @type Boolean
       * @final
       */
      webkit: webkit,

      /**
       * Constant that is more than zero if the browser is IE.
       *
       * @property ie
       * @type Boolean
       * @final
       */
      ie: ie,

      /**
       * Constant that is true if the browser is Gecko.
       *
       * @property gecko
       * @type Boolean
       * @final
       */
      gecko: gecko,

      /**
       * Constant that is true if the os is Mac OS.
       *
       * @property mac
       * @type Boolean
       * @final
       */
      mac: mac,

      /**
       * Constant that is true if the os is iOS.
       *
       * @property iOS
       * @type Boolean
       * @final
       */
      iOS: iDevice,

      /**
       * Constant that is true if the os is android.
       *
       * @property android
       * @type Boolean
       * @final
       */
      android: android,

      /**
       * Constant that is true if the browser supports editing.
       *
       * @property contentEditable
       * @type Boolean
       * @final
       */
      contentEditable: contentEditable,

      /**
       * Transparent image data url.
       *
       * @property transparentSrc
       * @type Boolean
       * @final
       */
      transparentSrc: "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",

      /**
       * Returns true/false if the browser can or can't place the caret after a inline block like an image.
       *
       * @property noCaretAfter
       * @type Boolean
       * @final
       */
      caretAfter: ie != 8,

      /**
       * Constant that is true if the browser supports native DOM Ranges. IE 9+.
       *
       * @property range
       * @type Boolean
       */
      range: window.getSelection && "Range" in window,

      /**
       * Returns the IE document mode for non IE browsers this will fake IE 10.
       *
       * @property documentMode
       * @type Number
       */
      documentMode: ie && !ie12 ? (document.documentMode || 7) : 10,

      /**
       * Constant that is true if the browser has a modern file api.
       *
       * @property fileApi
       * @type Boolean
       */
      fileApi: fileApi,

      /**
       * Constant that is true if the browser supports contentEditable=false regions.
       *
       * @property ceFalse
       * @type Boolean
       */
      ceFalse: (ie === false || ie > 8),

      /**
       * Constant if CSP mode is possible or not. Meaning we can't use script urls for the iframe.
       */
      canHaveCSP: (ie === false || ie > 11),

      desktop: !phone && !tablet,
      windowsPhone: windowsPhone
    };
  }
);

defineGlobal("global!clearInterval", clearInterval);
defineGlobal("global!clearTimeout", clearTimeout);
defineGlobal("global!setInterval", setInterval);
defineGlobal("global!setTimeout", setTimeout);
/**
 * Promise.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * Promise polyfill under MIT license: https://github.com/taylorhakes/promise-polyfill
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

/* eslint-disable */
/* jshint ignore:start */

/**
 * Modifed to be a feature fill and wrapped as tinymce module.
 */
define(
  'tinymce.core.util.Promise',
  [],
  function () {
    if (window.Promise) {
      return window.Promise;
    }

    // Use polyfill for setImmediate for performance gains
    var asap = Promise.immediateFn || (typeof setImmediate === 'function' && setImmediate) ||
      function (fn) { setTimeout(fn, 1); };

    // Polyfill for Function.prototype.bind
    function bind(fn, thisArg) {
      return function () {
        fn.apply(thisArg, arguments);
      };
    }

    var isArray = Array.isArray || function (value) { return Object.prototype.toString.call(value) === "[object Array]"; };

    function Promise(fn) {
      if (typeof this !== 'object') throw new TypeError('Promises must be constructed via new');
      if (typeof fn !== 'function') throw new TypeError('not a function');
      this._state = null;
      this._value = null;
      this._deferreds = [];

      doResolve(fn, bind(resolve, this), bind(reject, this));
    }

    function handle(deferred) {
      var me = this;
      if (this._state === null) {
        this._deferreds.push(deferred);
        return;
      }
      asap(function () {
        var cb = me._state ? deferred.onFulfilled : deferred.onRejected;
        if (cb === null) {
          (me._state ? deferred.resolve : deferred.reject)(me._value);
          return;
        }
        var ret;
        try {
          ret = cb(me._value);
        }
        catch (e) {
          deferred.reject(e);
          return;
        }
        deferred.resolve(ret);
      });
    }

    function resolve(newValue) {
      try { //Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure
        if (newValue === this) throw new TypeError('A promise cannot be resolved with itself.');
        if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
          var then = newValue.then;
          if (typeof then === 'function') {
            doResolve(bind(then, newValue), bind(resolve, this), bind(reject, this));
            return;
          }
        }
        this._state = true;
        this._value = newValue;
        finale.call(this);
      } catch (e) { reject.call(this, e); }
    }

    function reject(newValue) {
      this._state = false;
      this._value = newValue;
      finale.call(this);
    }

    function finale() {
      for (var i = 0, len = this._deferreds.length; i < len; i++) {
        handle.call(this, this._deferreds[i]);
      }
      this._deferreds = null;
    }

    function Handler(onFulfilled, onRejected, resolve, reject) {
      this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
      this.onRejected = typeof onRejected === 'function' ? onRejected : null;
      this.resolve = resolve;
      this.reject = reject;
    }

    /**
     * Take a potentially misbehaving resolver function and make sure
     * onFulfilled and onRejected are only called once.
     *
     * Makes no guarantees about asynchrony.
     */
    function doResolve(fn, onFulfilled, onRejected) {
      var done = false;
      try {
        fn(function (value) {
          if (done) return;
          done = true;
          onFulfilled(value);
        }, function (reason) {
          if (done) return;
          done = true;
          onRejected(reason);
        });
      } catch (ex) {
        if (done) return;
        done = true;
        onRejected(ex);
      }
    }

    Promise.prototype['catch'] = function (onRejected) {
      return this.then(null, onRejected);
    };

    Promise.prototype.then = function (onFulfilled, onRejected) {
      var me = this;
      return new Promise(function (resolve, reject) {
        handle.call(me, new Handler(onFulfilled, onRejected, resolve, reject));
      });
    };

    Promise.all = function () {
      var args = Array.prototype.slice.call(arguments.length === 1 && isArray(arguments[0]) ? arguments[0] : arguments);

      return new Promise(function (resolve, reject) {
        if (args.length === 0) return resolve([]);
        var remaining = args.length;
        function res(i, val) {
          try {
            if (val && (typeof val === 'object' || typeof val === 'function')) {
              var then = val.then;
              if (typeof then === 'function') {
                then.call(val, function (val) { res(i, val); }, reject);
                return;
              }
            }
            args[i] = val;
            if (--remaining === 0) {
              resolve(args);
            }
          } catch (ex) {
            reject(ex);
          }
        }
        for (var i = 0; i < args.length; i++) {
          res(i, args[i]);
        }
      });
    };

    Promise.resolve = function (value) {
      if (value && typeof value === 'object' && value.constructor === Promise) {
        return value;
      }

      return new Promise(function (resolve) {
        resolve(value);
      });
    };

    Promise.reject = function (value) {
      return new Promise(function (resolve, reject) {
        reject(value);
      });
    };

    Promise.race = function (values) {
      return new Promise(function (resolve, reject) {
        for (var i = 0, len = values.length; i < len; i++) {
          values[i].then(resolve, reject);
        }
      });
    };

    return Promise;
  }
);

/* jshint ignore:end */
/* eslint-enable */
/**
 * Delay.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
 */

/**
 * Utility class for working with delayed actions like setTimeout.
 *
 * @class tinymce.util.Delay
 */
define(
  'tinymce.core.util.Delay',
  [
    'global!clearInterval',
    'global!clearTimeout',
    'global!document',
    'global!setInterval',
    'global!setTimeout',
    'global!window',
    'tinymce.core.util.Promise'
  ],
  function (clearInterval, clearTimeout, document, setInterval, setTimeout, window, Promise) {
    var requestAnimationFramePromise;

    var requestAnimationFrame = function (callback, element) {
      var i, requestAnimationFrameFunc = window.requestAnimationFrame, vendors = ['ms', 'moz', 'webkit'];

      var featurefill = function (callback) {
        window.setTimeout(callback, 0);
      };

      for (i = 0; i < vendors.length && !requestAnimationFrameFunc; i++) {
        requestAnimationFrameFunc = window[vendors[i] + 'RequestAnimationFrame'];
      }

      if (!requestAnimationFrameFunc) {
        requestAnimationFrameFunc = featurefill;
      }

      requestAnimationFrameFunc(callback, element);
    };

    var wrappedSetTimeout = function (callback, time) {
      if (typeof time != 'number') {
        time = 0;
      }

      return setTimeout(callback, time);
    };

    var wrappedSetInterval = function (callback, time) {
      if (typeof time != 'number') {
        time = 1; // IE 8 needs it to be > 0
      }

      return setInterval(callback, time);
    };

    var wrappedClearTimeout = function (id) {
      return clearTimeout(id);
    };

    var wrappedClearInterval = function (id) {
      return clearInterval(id);
    };

    var debounce = function (callback, time) {
      var timer, func;

      func = function () {
        var args = arguments;

        clearTimeout(timer);

        timer = wrappedSetTimeout(function () {
          callback.apply(this, args);
        }, time);
      };

      func.stop = function () {
        clearTimeout(timer);
      };

      return func;
    };

    return {
      /**
       * Requests an animation frame and fallbacks to a timeout on older browsers.
       *
       * @method requestAnimationFrame
       * @param {function} callback Callback to execute when a new frame is available.
       * @param {DOMElement} element Optional element to scope it to.
       */
      requestAnimationFrame: function (callback, element) {
        if (requestAnimationFramePromise) {
          requestAnimationFramePromise.then(callback);
          return;
        }

        requestAnimationFramePromise = new Promise(function (resolve) {
          if (!element) {
            element = document.body;
          }

          requestAnimationFrame(resolve, element);
        }).then(callback);
      },

      /**
       * Sets a timer in ms and executes the specified callback when the timer runs out.
       *
       * @method setTimeout
       * @param {function} callback Callback to execute when timer runs out.
       * @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
       * @return {Number} Timeout id number.
       */
      setTimeout: wrappedSetTimeout,

      /**
       * Sets an interval timer in ms and executes the specified callback at every interval of that time.
       *
       * @method setInterval
       * @param {function} callback Callback to execute when interval time runs out.
       * @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
       * @return {Number} Timeout id number.
       */
      setInterval: wrappedSetInterval,

      /**
       * Sets an editor timeout it's similar to setTimeout except that it checks if the editor instance is
       * still alive when the callback gets executed.
       *
       * @method setEditorTimeout
       * @param {tinymce.Editor} editor Editor instance to check the removed state on.
       * @param {function} callback Callback to execute when timer runs out.
       * @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
       * @return {Number} Timeout id number.
       */
      setEditorTimeout: function (editor, callback, time) {
        return wrappedSetTimeout(function () {
          if (!editor.removed) {
            callback();
          }
        }, time);
      },

      /**
       * Sets an interval timer it's similar to setInterval except that it checks if the editor instance is
       * still alive when the callback gets executed.
       *
       * @method setEditorInterval
       * @param {function} callback Callback to execute when interval time runs out.
       * @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
       * @return {Number} Timeout id number.
       */
      setEditorInterval: function (editor, callback, time) {
        var timer;

        timer = wrappedSetInterval(function () {
          if (!editor.removed) {
            callback();
          } else {
            clearInterval(timer);
          }
        }, time);

        return timer;
      },

      /**
       * Creates debounced callback function that only gets executed once within the specified time.
       *
       * @method debounce
       * @param {function} callback Callback to execute when timer finishes.
       * @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
       * @return {Function} debounced function callback.
       */
      debounce: debounce,

      // Throttle needs to be debounce due to backwards compatibility.
      throttle: debounce,

      /**
       * Clears an interval timer so it won't execute.
       *
       * @method clearInterval
       * @param {Number} Interval timer id number.
       */
      clearInterval: wrappedClearInterval,

      /**
       * Clears an timeout timer so it won't execute.
       *
       * @method clearTimeout
       * @param {Number} Timeout timer id number.
       */
      clearTimeout: wrappedClearTimeout
    };
  }
);

/**
 * EventUtils.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
 */

/*jshint loopfunc:true*/
/*eslint no-loop-func:0 */

/**
 * This class wraps the browsers native event logic with more convenient methods.
 *
 * @class tinymce.dom.EventUtils
 */
define(
  'tinymce.core.dom.EventUtils',
  [
    'global!document',
    'global!window',
    'tinymce.core.Env',
    'tinymce.core.util.Delay'
  ],
  function (document, window, Env, Delay) {
    "use strict";

    var eventExpandoPrefix = "mce-data-";
    var mouseEventRe = /^(?:mouse|contextmenu)|click/;
    var deprecated = {
      keyLocation: 1, layerX: 1, layerY: 1, returnValue: 1,
      webkitMovementX: 1, webkitMovementY: 1, keyIdentifier: 1
    };

    // Checks if it is our own isDefaultPrevented function
    var hasIsDefaultPrevented = function (event) {
      return event.isDefaultPrevented === returnTrue || event.isDefaultPrevented === returnFalse;
    };

    // Dummy function that gets replaced on the delegation state functions
    var returnFalse = function () {
      return false;
    };

    // Dummy function that gets replaced on the delegation state functions
    var returnTrue = function () {
      return true;
    };

    /**
     * Binds a native event to a callback on the speified target.
     */
    var addEvent = function (target, name, callback, capture) {
      if (target.addEventListener) {
        target.addEventListener(name, callback, capture || false);
      } else if (target.attachEvent) {
        target.attachEvent('on' + name, callback);
      }
    };

    /**
     * Unbinds a native event callback on the specified target.
     */
    var removeEvent = function (target, name, callback, capture) {
      if (target.removeEventListener) {
        target.removeEventListener(name, callback, capture || false);
      } else if (target.detachEvent) {
        target.detachEvent('on' + name, callback);
      }
    };

    /**
     * Gets the event target based on shadow dom properties like path and deepPath.
     */
    var getTargetFromShadowDom = function (event, defaultTarget) {
      var path, target = defaultTarget;

      // When target element is inside Shadow DOM we need to take first element from path
      // otherwise we'll get Shadow Root parent, not actual target element

      // Normalize target for WebComponents v0 implementation (in Chrome)
      path = event.path;
      if (path && path.length > 0) {
        target = path[0];
      }

      // Normalize target for WebComponents v1 implementation (standard)
      if (event.deepPath) {
        path = event.deepPath();
        if (path && path.length > 0) {
          target = path[0];
        }
      }

      return target;
    };

    /**
     * Normalizes a native event object or just adds the event specific methods on a custom event.
     */
    var fix = function (originalEvent, data) {
      var name, event = data || {}, undef;

      // Copy all properties from the original event
      for (name in originalEvent) {
        // layerX/layerY is deprecated in Chrome and produces a warning
        if (!deprecated[name]) {
          event[name] = originalEvent[name];
        }
      }

      // Normalize target IE uses srcElement
      if (!event.target) {
        event.target = event.srcElement || document;
      }

      // Experimental shadow dom support
      if (Env.experimentalShadowDom) {
        event.target = getTargetFromShadowDom(originalEvent, event.target);
      }

      // Calculate pageX/Y if missing and clientX/Y available
      if (originalEvent && mouseEventRe.test(originalEvent.type) && originalEvent.pageX === undef && originalEvent.clientX !== undef) {
        var eventDoc = event.target.ownerDocument || document;
        var doc = eventDoc.documentElement;
        var body = eventDoc.body;

        event.pageX = originalEvent.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) -
          (doc && doc.clientLeft || body && body.clientLeft || 0);

        event.pageY = originalEvent.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) -
          (doc && doc.clientTop || body && body.clientTop || 0);
      }

      // Add preventDefault method
      event.preventDefault = function () {
        event.isDefaultPrevented = returnTrue;

        // Execute preventDefault on the original event object
        if (originalEvent) {
          if (originalEvent.preventDefault) {
            originalEvent.preventDefault();
          } else {
            originalEvent.returnValue = false; // IE
          }
        }
      };

      // Add stopPropagation
      event.stopPropagation = function () {
        event.isPropagationStopped = returnTrue;

        // Execute stopPropagation on the original event object
        if (originalEvent) {
          if (originalEvent.stopPropagation) {
            originalEvent.stopPropagation();
          } else {
            originalEvent.cancelBubble = true; // IE
          }
        }
      };

      // Add stopImmediatePropagation
      event.stopImmediatePropagation = function () {
        event.isImmediatePropagationStopped = returnTrue;
        event.stopPropagation();
      };

      // Add event delegation states
      if (hasIsDefaultPrevented(event) === false) {
        event.isDefaultPrevented = returnFalse;
        event.isPropagationStopped = returnFalse;
        event.isImmediatePropagationStopped = returnFalse;
      }

      // Add missing metaKey for IE 8
      if (typeof event.metaKey == 'undefined') {
        event.metaKey = false;
      }

      return event;
    };

    /**
     * Bind a DOMContentLoaded event across browsers and executes the callback once the page DOM is initialized.
     * It will also set/check the domLoaded state of the event_utils instance so ready isn't called multiple times.
     */
    var bindOnReady = function (win, callback, eventUtils) {
      var doc = win.document, event = { type: 'ready' };

      if (eventUtils.domLoaded) {
        callback(event);
        return;
      }

      var isDocReady = function () {
        // Check complete or interactive state if there is a body
        // element on some iframes IE 8 will produce a null body
        return doc.readyState === "complete" || (doc.readyState === "interactive" && doc.body);
      };

      // Gets called when the DOM is ready
      var readyHandler = function () {
        if (!eventUtils.domLoaded) {
          eventUtils.domLoaded = true;
          callback(event);
        }
      };

      var waitForDomLoaded = function () {
        if (isDocReady()) {
          removeEvent(doc, "readystatechange", waitForDomLoaded);
          readyHandler();
        }
      };

      var tryScroll = function () {
        try {
          // If IE is used, use the trick by Diego Perini licensed under MIT by request to the author.
          // http://javascript.nwbox.com/IEContentLoaded/
          doc.documentElement.doScroll("left");
        } catch (ex) {
          Delay.setTimeout(tryScroll);
          return;
        }

        readyHandler();
      };

      // Use W3C method (exclude IE 9,10 - readyState "interactive" became valid only in IE 11)
      if (doc.addEventListener && !(Env.ie && Env.ie < 11)) {
        if (isDocReady()) {
          readyHandler();
        } else {
          addEvent(win, 'DOMContentLoaded', readyHandler);
        }
      } else {
        // Use IE method
        addEvent(doc, "readystatechange", waitForDomLoaded);

        // Wait until we can scroll, when we can the DOM is initialized
        if (doc.documentElement.doScroll && win.self === win.top) {
          tryScroll();
        }
      }

      // Fallback if any of the above methods should fail for some odd reason
      addEvent(win, 'load', readyHandler);
    };

    /**
     * This class enables you to bind/unbind native events to elements and normalize it's behavior across browsers.
     */
    var EventUtils = function () {
      var self = this, events = {}, count, expando, hasFocusIn, hasMouseEnterLeave, mouseEnterLeave;

      expando = eventExpandoPrefix + (+new Date()).toString(32);
      hasMouseEnterLeave = "onmouseenter" in document.documentElement;
      hasFocusIn = "onfocusin" in document.documentElement;
      mouseEnterLeave = { mouseenter: 'mouseover', mouseleave: 'mouseout' };
      count = 1;

      // State if the DOMContentLoaded was executed or not
      self.domLoaded = false;
      self.events = events;

      /**
       * Executes all event handler callbacks for a specific event.
       *
       * @private
       * @param {Event} evt Event object.
       * @param {String} id Expando id value to look for.
       */
      var executeHandlers = function (evt, id) {
        var callbackList, i, l, callback, container = events[id];

        callbackList = container && container[evt.type];
        if (callbackList) {
          for (i = 0, l = callbackList.length; i < l; i++) {
            callback = callbackList[i];

            // Check if callback exists might be removed if a unbind is called inside the callback
            if (callback && callback.func.call(callback.scope, evt) === false) {
              evt.preventDefault();
            }

            // Should we stop propagation to immediate listeners
            if (evt.isImmediatePropagationStopped()) {
              return;
            }
          }
        }
      };

      /**
       * Binds a callback to an event on the specified target.
       *
       * @method bind
       * @param {Object} target Target node/window or custom object.
       * @param {String} names Name of the event to bind.
       * @param {function} callback Callback function to execute when the event occurs.
       * @param {Object} scope Scope to call the callback function on, defaults to target.
       * @return {function} Callback function that got bound.
       */
      self.bind = function (target, names, callback, scope) {
        var id, callbackList, i, name, fakeName, nativeHandler, capture, win = window;

        // Native event handler function patches the event and executes the callbacks for the expando
        var defaultNativeHandler = function (evt) {
          executeHandlers(fix(evt || win.event), id);
        };

        // Don't bind to text nodes or comments
        if (!target || target.nodeType === 3 || target.nodeType === 8) {
          return;
        }

        // Create or get events id for the target
        if (!target[expando]) {
          id = count++;
          target[expando] = id;
          events[id] = {};
        } else {
          id = target[expando];
        }

        // Setup the specified scope or use the target as a default
        scope = scope || target;

        // Split names and bind each event, enables you to bind multiple events with one call
        names = names.split(' ');
        i = names.length;
        while (i--) {
          name = names[i];
          nativeHandler = defaultNativeHandler;
          fakeName = capture = false;

          // Use ready instead of DOMContentLoaded
          if (name === "DOMContentLoaded") {
            name = "ready";
          }

          // DOM is already ready
          if (self.domLoaded && name === "ready" && target.readyState == 'complete') {
            callback.call(scope, fix({ type: name }));
            continue;
          }

          // Handle mouseenter/mouseleaver
          if (!hasMouseEnterLeave) {
            fakeName = mouseEnterLeave[name];

            if (fakeName) {
              nativeHandler = function (evt) {
                var current, related;

                current = evt.currentTarget;
                related = evt.relatedTarget;

                // Check if related is inside the current target if it's not then the event should
                // be ignored since it's a mouseover/mouseout inside the element
                if (related && current.contains) {
                  // Use contains for performance
                  related = current.contains(related);
                } else {
                  while (related && related !== current) {
                    related = related.parentNode;
                  }
                }

                // Fire fake event
                if (!related) {
                  evt = fix(evt || win.event);
                  evt.type = evt.type === 'mouseout' ? 'mouseleave' : 'mouseenter';
                  evt.target = current;
                  executeHandlers(evt, id);
                }
              };
            }
          }

          // Fake bubbling of focusin/focusout
          if (!hasFocusIn && (name === "focusin" || name === "focusout")) {
            capture = true;
            fakeName = name === "focusin" ? "focus" : "blur";
            nativeHandler = function (evt) {
              evt = fix(evt || win.event);
              evt.type = evt.type === 'focus' ? 'focusin' : 'focusout';
              executeHandlers(evt, id);
            };
          }

          // Setup callback list and bind native event
          callbackList = events[id][name];
          if (!callbackList) {
            events[id][name] = callbackList = [{ func: callback, scope: scope }];
            callbackList.fakeName = fakeName;
            callbackList.capture = capture;
            //callbackList.callback = callback;

            // Add the nativeHandler to the callback list so that we can later unbind it
            callbackList.nativeHandler = nativeHandler;

            // Check if the target has native events support

            if (name === "ready") {
              bindOnReady(target, nativeHandler, self);
            } else {
              addEvent(target, fakeName || name, nativeHandler, capture);
            }
          } else {
            if (name === "ready" && self.domLoaded) {
              callback({ type: name });
            } else {
              // If it already has an native handler then just push the callback
              callbackList.push({ func: callback, scope: scope });
            }
          }
        }

        target = callbackList = 0; // Clean memory for IE

        return callback;
      };

      /**
       * Unbinds the specified event by name, name and callback or all events on the target.
       *
       * @method unbind
       * @param {Object} target Target node/window or custom object.
       * @param {String} names Optional event name to unbind.
       * @param {function} callback Optional callback function to unbind.
       * @return {EventUtils} Event utils instance.
       */
      self.unbind = function (target, names, callback) {
        var id, callbackList, i, ci, name, eventMap;

        // Don't bind to text nodes or comments
        if (!target || target.nodeType === 3 || target.nodeType === 8) {
          return self;
        }

        // Unbind event or events if the target has the expando
        id = target[expando];
        if (id) {
          eventMap = events[id];

          // Specific callback
          if (names) {
            names = names.split(' ');
            i = names.length;
            while (i--) {
              name = names[i];
              callbackList = eventMap[name];

              // Unbind the event if it exists in the map
              if (callbackList) {
                // Remove specified callback
                if (callback) {
                  ci = callbackList.length;
                  while (ci--) {
                    if (callbackList[ci].func === callback) {
                      var nativeHandler = callbackList.nativeHandler;
                      var fakeName = callbackList.fakeName, capture = callbackList.capture;

                      // Clone callbackList since unbind inside a callback would otherwise break the handlers loop
                      callbackList = callbackList.slice(0, ci).concat(callbackList.slice(ci + 1));
                      callbackList.nativeHandler = nativeHandler;
                      callbackList.fakeName = fakeName;
                      callbackList.capture = capture;

                      eventMap[name] = callbackList;
                    }
                  }
                }

                // Remove all callbacks if there isn't a specified callback or there is no callbacks left
                if (!callback || callbackList.length === 0) {
                  delete eventMap[name];
                  removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture);
                }
              }
            }
          } else {
            // All events for a specific element
            for (name in eventMap) {
              callbackList = eventMap[name];
              removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture);
            }

            eventMap = {};
          }

          // Check if object is empty, if it isn't then we won't remove the expando map
          for (name in eventMap) {
            return self;
          }

          // Delete event object
          delete events[id];

          // Remove expando from target
          try {
            // IE will fail here since it can't delete properties from window
            delete target[expando];
          } catch (ex) {
            // IE will set it to null
            target[expando] = null;
          }
        }

        return self;
      };

      /**
       * Fires the specified event on the specified target.
       *
       * @method fire
       * @param {Object} target Target node/window or custom object.
       * @param {String} name Event name to fire.
       * @param {Object} args Optional arguments to send to the observers.
       * @return {EventUtils} Event utils instance.
       */
      self.fire = function (target, name, args) {
        var id;

        // Don't bind to text nodes or comments
        if (!target || target.nodeType === 3 || target.nodeType === 8) {
          return self;
        }

        // Build event object by patching the args
        args = fix(null, args);
        args.type = name;
        args.target = target;

        do {
          // Found an expando that means there is listeners to execute
          id = target[expando];
          if (id) {
            executeHandlers(args, id);
          }

          // Walk up the DOM
          target = target.parentNode || target.ownerDocument || target.defaultView || target.parentWindow;
        } while (target && !args.isPropagationStopped());

        return self;
      };

      /**
       * Removes all bound event listeners for the specified target. This will also remove any bound
       * listeners to child nodes within that target.
       *
       * @method clean
       * @param {Object} target Target node/window object.
       * @return {EventUtils} Event utils instance.
       */
      self.clean = function (target) {
        var i, children, unbind = self.unbind;

        // Don't bind to text nodes or comments
        if (!target || target.nodeType === 3 || target.nodeType === 8) {
          return self;
        }

        // Unbind any element on the specified target
        if (target[expando]) {
          unbind(target);
        }

        // Target doesn't have getElementsByTagName it's probably a window object then use it's document to find the children
        if (!target.getElementsByTagName) {
          target = target.document;
        }

        // Remove events from each child element
        if (target && target.getElementsByTagName) {
          unbind(target);

          children = target.getElementsByTagName('*');
          i = children.length;
          while (i--) {
            target = children[i];

            if (target[expando]) {
              unbind(target);
            }
          }
        }

        return self;
      };

      /**
       * Destroys the event object. Call this on IE to remove memory leaks.
       */
      self.destroy = function () {
        events = {};
      };

      // Legacy function for canceling events
      self.cancel = function (e) {
        if (e) {
          e.preventDefault();
          e.stopImmediatePropagation();
        }

        return false;
      };
    };

    EventUtils.Event = new EventUtils();
    EventUtils.Event.bind(window, 'ready', function () { });

    return EventUtils;
  }
);

/**
 * Sizzle.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
 *
 * @ignore-file
 *
 * Forked changes:
 * - Disabled all assertions since they are only used for non supported browsers and cause dom repaints see #TINY-1141
 */

/*jshint bitwise:false, expr:true, noempty:false, sub:true, eqnull:true, latedef:false, maxlen:255 */
/*eslint-disable */

/**
 * Sizzle CSS Selector Engine v@VERSION
 * http://sizzlejs.com/
 *
 * Copyright 2008, 2014 jQuery Foundation, Inc. and other contributors
 * Released under the MIT license
 * http://jquery.org/license
 *
 * Date: @DATE
 */
define(
  'tinymce.core.dom.Sizzle',
  [],
  function () {
    var i,
      support,
      Expr,
      getText,
      isXML,
      tokenize,
      compile,
      select,
      outermostContext,
      sortInput,
      hasDuplicate,

      // Local document vars
      setDocument,
      document,
      docElem,
      documentIsHTML,
      rbuggyQSA,
      rbuggyMatches,
      matches,
      contains,

      // Instance-specific data
      expando = "sizzle" + -(new Date()),
      preferredDoc = window.document,
      dirruns = 0,
      done = 0,
      classCache = createCache(),
      tokenCache = createCache(),
      compilerCache = createCache(),
      sortOrder = function (a, b) {
        if (a === b) {
          hasDuplicate = true;
        }
        return 0;
      },

      // General-purpose constants
      strundefined = typeof undefined,
      MAX_NEGATIVE = 1 << 31,

      // Instance methods
      hasOwn = ({}).hasOwnProperty,
      arr = [],
      pop = arr.pop,
      push_native = arr.push,
      push = arr.push,
      slice = arr.slice,
      // Use a stripped-down indexOf if we can't use a native one
      indexOf = arr.indexOf || function (elem) {
        var i = 0,
          len = this.length;
        for (; i < len; i++) {
          if (this[i] === elem) {
            return i;
          }
        }
        return -1;
      },

      booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",

      // Regular expressions

      // http://www.w3.org/TR/css3-selectors/#whitespace
      whitespace = "[\\x20\\t\\r\\n\\f]",

      // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
      identifier = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",

      // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors
      attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace +
        // Operator (capture 2)
        "*([*^$|!~]?=)" + whitespace +
        // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]"
        "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace +
        "*\\]",

      pseudos = ":(" + identifier + ")(?:\\((" +
        // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:
        // 1. quoted (capture 3; capture 4 or capture 5)
        "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" +
        // 2. simple (capture 6)
        "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" +
        // 3. anything else (capture 2)
        ".*" +
        ")\\)|)",

      // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
      rtrim = new RegExp("^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g"),

      rcomma = new RegExp("^" + whitespace + "*," + whitespace + "*"),
      rcombinators = new RegExp("^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*"),

      rattributeQuotes = new RegExp("=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g"),

      rpseudo = new RegExp(pseudos),
      ridentifier = new RegExp("^" + identifier + "$"),

      matchExpr = {
        "ID": new RegExp("^#(" + identifier + ")"),
        "CLASS": new RegExp("^\\.(" + identifier + ")"),
        "TAG": new RegExp("^(" + identifier + "|[*])"),
        "ATTR": new RegExp("^" + attributes),
        "PSEUDO": new RegExp("^" + pseudos),
        "CHILD": new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
          "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
          "*(\\d+)|))" + whitespace + "*\\)|)", "i"),
        "bool": new RegExp("^(?:" + booleans + ")$", "i"),
        // For use in libraries implementing .is()
        // We use this for POS matching in `select`
        "needsContext": new RegExp("^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
          whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i")
      },

      rinputs = /^(?:input|select|textarea|button)$/i,
      rheader = /^h\d$/i,

      rnative = /^[^{]+\{\s*\[native \w/,

      // Easily-parseable/retrievable ID or TAG or CLASS selectors
      rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,

      rsibling = /[+~]/,
      rescape = /'|\\/g,

      // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
      runescape = new RegExp("\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig"),
      funescape = function (_, escaped, escapedWhitespace) {
        var high = "0x" + escaped - 0x10000;
        // NaN means non-codepoint
        // Support: Firefox<24
        // Workaround erroneous numeric interpretation of +"0x"
        return high !== high || escapedWhitespace ?
          escaped :
          high < 0 ?
            // BMP codepoint
            String.fromCharCode(high + 0x10000) :
            // Supplemental Plane codepoint (surrogate pair)
            String.fromCharCode(high >> 10 | 0xD800, high & 0x3FF | 0xDC00);
      };

    // Optimize for push.apply( _, NodeList )
    try {
      push.apply(
        (arr = slice.call(preferredDoc.childNodes)),
        preferredDoc.childNodes
      );
      // Support: Android<4.0
      // Detect silently failing push.apply
      arr[preferredDoc.childNodes.length].nodeType;
    } catch (e) {
      push = {
        apply: arr.length ?

          // Leverage slice if possible
          function (target, els) {
            push_native.apply(target, slice.call(els));
          } :

          // Support: IE<9
          // Otherwise append directly
          function (target, els) {
            var j = target.length,
              i = 0;
            // Can't trust NodeList.length
            while ((target[j++] = els[i++])) { }
            target.length = j - 1;
          }
      };
    }

    function Sizzle(selector, context, results, seed) {
      var match, elem, m, nodeType,
        // QSA vars
        i, groups, old, nid, newContext, newSelector;

      if ((context ? context.ownerDocument || context : preferredDoc) !== document) {
        setDocument(context);
      }

      context = context || document;
      results = results || [];

      if (!selector || typeof selector !== "string") {
        return results;
      }

      if ((nodeType = context.nodeType) !== 1 && nodeType !== 9) {
        return [];
      }

      if (documentIsHTML && !seed) {

        // Shortcuts
        if ((match = rquickExpr.exec(selector))) {
          // Speed-up: Sizzle("#ID")
          if ((m = match[1])) {
            if (nodeType === 9) {
              elem = context.getElementById(m);
              // Check parentNode to catch when Blackberry 4.6 returns
              // nodes that are no longer in the document (jQuery #6963)
              if (elem && elem.parentNode) {
                // Handle the case where IE, Opera, and Webkit return items
                // by name instead of ID
                if (elem.id === m) {
                  results.push(elem);
                  return results;
                }
              } else {
                return results;
              }
            } else {
              // Context is not a document
              if (context.ownerDocument && (elem = context.ownerDocument.getElementById(m)) &&
                contains(context, elem) && elem.id === m) {
                results.push(elem);
                return results;
              }
            }

            // Speed-up: Sizzle("TAG")
          } else if (match[2]) {
            push.apply(results, context.getElementsByTagName(selector));
            return results;

            // Speed-up: Sizzle(".CLASS")
          } else if ((m = match[3]) && support.getElementsByClassName) {
            push.apply(results, context.getElementsByClassName(m));
            return results;
          }
        }

        // QSA path
        if (support.qsa && (!rbuggyQSA || !rbuggyQSA.test(selector))) {
          nid = old = expando;
          newContext = context;
          newSelector = nodeType === 9 && selector;

          // qSA works strangely on Element-rooted queries
          // We can work around this by specifying an extra ID on the root
          // and working up from there (Thanks to Andrew Dupont for the technique)
          // IE 8 doesn't work on object elements
          if (nodeType === 1 && context.nodeName.toLowerCase() !== "object") {
            groups = tokenize(selector);

            if ((old = context.getAttribute("id"))) {
              nid = old.replace(rescape, "\\$&");
            } else {
              context.setAttribute("id", nid);
            }
            nid = "[id='" + nid + "'] ";

            i = groups.length;
            while (i--) {
              groups[i] = nid + toSelector(groups[i]);
            }
            newContext = rsibling.test(selector) && testContext(context.parentNode) || context;
            newSelector = groups.join(",");
          }

          if (newSelector) {
            try {
              push.apply(results,
                newContext.querySelectorAll(newSelector)
              );
              return results;
            } catch (qsaError) {
            } finally {
              if (!old) {
                context.removeAttribute("id");
              }
            }
          }
        }
      }

      // All others
      return select(selector.replace(rtrim, "$1"), context, results, seed);
    }

    /**
     * Create key-value caches of limited size
     * @returns {Function(string, Object)} Returns the Object data after storing it on itself with
     * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
     * deleting the oldest entry
     */
    function createCache() {
      var keys = [];

      function cache(key, value) {
        // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
        if (keys.push(key + " ") > Expr.cacheLength) {
          // Only keep the most recent entries
          delete cache[keys.shift()];
        }
        return (cache[key + " "] = value);
      }
      return cache;
    }

    /**
     * Mark a function for special use by Sizzle
     * @param {Function} fn The function to mark
     */
    function markFunction(fn) {
      fn[expando] = true;
      return fn;
    }

    /**
     * Support testing using an element
     * @param {Function} fn Passed the created div and expects a boolean result
     */
    /*function assert(fn) {
      var div = document.createElement("div");

      try {
        return !!fn(div);
      } catch (e) {
        return false;
      } finally {
        // Remove from its parent by default
        if (div.parentNode) {
          div.parentNode.removeChild(div);
        }
        // release memory in IE
        div = null;
      }
    }*/

    /**
     * Adds the same handler for all of the specified attrs
     * @param {String} attrs Pipe-separated list of attributes
     * @param {Function} handler The method that will be applied
     */
    function addHandle(attrs, handler) {
      var arr = attrs.split("|"),
        i = attrs.length;

      while (i--) {
        Expr.attrHandle[arr[i]] = handler;
      }
    }

    /**
     * Checks document order of two siblings
     * @param {Element} a
     * @param {Element} b
     * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b
     */
    function siblingCheck(a, b) {
      var cur = b && a,
        diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
          (~b.sourceIndex || MAX_NEGATIVE) -
          (~a.sourceIndex || MAX_NEGATIVE);

      // Use IE sourceIndex if available on both nodes
      if (diff) {
        return diff;
      }

      // Check if b follows a
      if (cur) {
        while ((cur = cur.nextSibling)) {
          if (cur === b) {
            return -1;
          }
        }
      }

      return a ? 1 : -1;
    }

    /**
     * Returns a function to use in pseudos for input types
     * @param {String} type
     */
    function createInputPseudo(type) {
      return function (elem) {
        var name = elem.nodeName.toLowerCase();
        return name === "input" && elem.type === type;
      };
    }

    /**
     * Returns a function to use in pseudos for buttons
     * @param {String} type
     */
    function createButtonPseudo(type) {
      return function (elem) {
        var name = elem.nodeName.toLowerCase();
        return (name === "input" || name === "button") && elem.type === type;
      };
    }

    /**
     * Returns a function to use in pseudos for positionals
     * @param {Function} fn
     */
    function createPositionalPseudo(fn) {
      return markFunction(function (argument) {
        argument = +argument;
        return markFunction(function (seed, matches) {
          var j,
            matchIndexes = fn([], seed.length, argument),
            i = matchIndexes.length;

          // Match elements found at the specified indexes
          while (i--) {
            if (seed[(j = matchIndexes[i])]) {
              seed[j] = !(matches[j] = seed[j]);
            }
          }
        });
      });
    }

    /**
     * Checks a node for validity as a Sizzle context
     * @param {Element|Object=} context
     * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value
     */
    function testContext(context) {
      return context && typeof context.getElementsByTagName !== strundefined && context;
    }

    // Expose support vars for convenience
    support = Sizzle.support = {};

    /**
     * Detects XML nodes
     * @param {Element|Object} elem An element or a document
     * @returns {Boolean} True iff elem is a non-HTML XML node
     */
    isXML = Sizzle.isXML = function (elem) {
      // documentElement is verified for cases where it doesn't yet exist
      // (such as loading iframes in IE - #4833)
      var documentElement = elem && (elem.ownerDocument || elem).documentElement;
      return documentElement ? documentElement.nodeName !== "HTML" : false;
    };

    /**
     * Sets document-related variables once based on the current document
     * @param {Element|Object} [doc] An element or document object to use to set the document
     * @returns {Object} Returns the current document
     */
    setDocument = Sizzle.setDocument = function (node) {
      var hasCompare,
        doc = node ? node.ownerDocument || node : preferredDoc,
        parent = doc.defaultView;

      function getTop(win) {
        // Edge throws a lovely Object expected if you try to get top on a detached reference see #2642
        try {
          return win.top;
        } catch (ex) {
          // Ignore
        }

        return null;
      }

      // If no document and documentElement is available, return
      if (doc === document || doc.nodeType !== 9 || !doc.documentElement) {
        return document;
      }

      // Set our document
      document = doc;
      docElem = doc.documentElement;

      // Support tests
      documentIsHTML = !isXML(doc);

      // Support: IE>8
      // If iframe document is assigned to "document" variable and if iframe has been reloaded,
      // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936
      // IE6-8 do not support the defaultView property so parent will be undefined
      if (parent && parent !== getTop(parent)) {
        // IE11 does not have attachEvent, so all must suffer
        if (parent.addEventListener) {
          parent.addEventListener("unload", function () {
            setDocument();
          }, false);
        } else if (parent.attachEvent) {
          parent.attachEvent("onunload", function () {
            setDocument();
          });
        }
      }

      /* Attributes
      ---------------------------------------------------------------------- */

      // Support: IE<8
      // Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans)
      support.attributes = true;

      /* getElement(s)By*
      ---------------------------------------------------------------------- */

      // Check if getElementsByTagName("*") returns only elements
      support.getElementsByTagName = true;

      // Support: IE<9
      support.getElementsByClassName = rnative.test(doc.getElementsByClassName);

      // Support: IE<10
      // Check if getElementById returns elements by name
      // The broken getElementById methods don't pick up programatically-set names,
      // so use a roundabout getElementsByName test
      support.getById = true;

      // ID find and filter
      /*if (support.getById) {*/
        Expr.find["ID"] = function (id, context) {
          if (typeof context.getElementById !== strundefined && documentIsHTML) {
            var m = context.getElementById(id);
            // Check parentNode to catch when Blackberry 4.6 returns
            // nodes that are no longer in the document #6963
            return m && m.parentNode ? [m] : [];
          }
        };
        Expr.filter["ID"] = function (id) {
          var attrId = id.replace(runescape, funescape);
          return function (elem) {
            return elem.getAttribute("id") === attrId;
          };
        };
      /*} else {
        // Support: IE6/7
        // getElementById is not reliable as a find shortcut
        delete Expr.find["ID"];

        Expr.filter["ID"] = function (id) {
          var attrId = id.replace(runescape, funescape);
          return function (elem) {
            var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id");
            return node && node.value === attrId;
          };
        };
      }*/

      // Tag
      Expr.find["TAG"] = support.getElementsByTagName ?
        function (tag, context) {
          if (typeof context.getElementsByTagName !== strundefined) {
            return context.getElementsByTagName(tag);
          }
        } :
        function (tag, context) {
          var elem,
            tmp = [],
            i = 0,
            results = context.getElementsByTagName(tag);

          // Filter out possible comments
          if (tag === "*") {
            while ((elem = results[i++])) {
              if (elem.nodeType === 1) {
                tmp.push(elem);
              }
            }

            return tmp;
          }
          return results;
        };

      // Class
      Expr.find["CLASS"] = support.getElementsByClassName && function (className, context) {
        if (documentIsHTML) {
          return context.getElementsByClassName(className);
        }
      };

      /* QSA/matchesSelector
      ---------------------------------------------------------------------- */

      // QSA and matchesSelector support

      // matchesSelector(:active) reports false when true (IE9/Opera 11.5)
      rbuggyMatches = [];

      // qSa(:focus) reports false when true (Chrome 21)
      // We allow this because of a bug in IE8/9 that throws an error
      // whenever `document.activeElement` is accessed on an iframe
      // So, we allow :focus to pass through QSA all the time to avoid the IE error
      // See http://bugs.jquery.com/ticket/13378
      rbuggyQSA = [];

      /*
      if ((support.qsa = rnative.test(doc.querySelectorAll))) {
        // Build QSA regex
        // Regex strategy adopted from Diego Perini
        assert(function (div) {
          // Select is set to empty string on purpose
          // This is to test IE's treatment of not explicitly
          // setting a boolean content attribute,
          // since its presence should be enough
          // http://bugs.jquery.com/ticket/12359
          div.innerHTML = "<select msallowcapture=''><option selected=''></option></select>";

          // Support: IE8, Opera 11-12.16
          // Nothing should be selected when empty strings follow ^= or $= or *=
          // The test attribute must be unknown in Opera but "safe" for WinRT
          // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section
          if (div.querySelectorAll("[msallowcapture^='']").length) {
            rbuggyQSA.push("[*^$]=" + whitespace + "*(?:''|\"\")");
          }

          // Support: IE8
          // Boolean attributes and "value" are not treated correctly
          if (!div.querySelectorAll("[selected]").length) {
            rbuggyQSA.push("\\[" + whitespace + "*(?:value|" + booleans + ")");
          }

          // Webkit/Opera - :checked should return selected option elements
          // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
          // IE8 throws error here and will not see later tests
          if (!div.querySelectorAll(":checked").length) {
            rbuggyQSA.push(":checked");
          }
        });

        assert(function (div) {
          // Support: Windows 8 Native Apps
          // The type and name attributes are restricted during .innerHTML assignment
          var input = doc.createElement("input");
          input.setAttribute("type", "hidden");
          div.appendChild(input).setAttribute("name", "D");

          // Support: IE8
          // Enforce case-sensitivity of name attribute
          if (div.querySelectorAll("[name=d]").length) {
            rbuggyQSA.push("name" + whitespace + "*[*^$|!~]?=");
          }

          // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
          // IE8 throws error here and will not see later tests
          if (!div.querySelectorAll(":enabled").length) {
            rbuggyQSA.push(":enabled", ":disabled");
          }

          // Opera 10-11 does not throw on post-comma invalid pseudos
          div.querySelectorAll("*,:x");
          rbuggyQSA.push(",.*:");
        });
      }
      */

      support.disconnectedMatch = true;

      /*
      if ((support.matchesSelector = rnative.test((matches = docElem.matches ||
        docElem.webkitMatchesSelector ||
        docElem.mozMatchesSelector ||
        docElem.oMatchesSelector ||
        docElem.msMatchesSelector)))) {

        assert(function (div) {
          // Check to see if it's possible to do matchesSelector
          // on a disconnected node (IE 9)
          support.disconnectedMatch = matches.call(div, "div");

          // This should fail with an exception
          // Gecko does not error, returns false instead
          matches.call(div, "[s!='']:x");
          rbuggyMatches.push("!=", pseudos);
        });
      }
      */

      rbuggyQSA = rbuggyQSA.length && new RegExp(rbuggyQSA.join("|"));
      rbuggyMatches = rbuggyMatches.length && new RegExp(rbuggyMatches.join("|"));

      /* Contains
      ---------------------------------------------------------------------- */
      hasCompare = rnative.test(docElem.compareDocumentPosition);

      // Element contains another
      // Purposefully does not implement inclusive descendent
      // As in, an element does not contain itself
      contains = hasCompare || rnative.test(docElem.contains) ?
        function (a, b) {
          var adown = a.nodeType === 9 ? a.documentElement : a,
            bup = b && b.parentNode;
          return a === bup || !!(bup && bup.nodeType === 1 && (
            adown.contains ?
              adown.contains(bup) :
              a.compareDocumentPosition && a.compareDocumentPosition(bup) & 16
          ));
        } :
        function (a, b) {
          if (b) {
            while ((b = b.parentNode)) {
              if (b === a) {
                return true;
              }
            }
          }
          return false;
        };

      /* Sorting
      ---------------------------------------------------------------------- */

      // Document order sorting
      sortOrder = hasCompare ?
        function (a, b) {

          // Flag for duplicate removal
          if (a === b) {
            hasDuplicate = true;
            return 0;
          }

          // Sort on method existence if only one input has compareDocumentPosition
          var compare = !a.compareDocumentPosition - !b.compareDocumentPosition;
          if (compare) {
            return compare;
          }

          // Calculate position if both inputs belong to the same document
          compare = (a.ownerDocument || a) === (b.ownerDocument || b) ?
            a.compareDocumentPosition(b) :

            // Otherwise we know they are disconnected
            1;

          // Disconnected nodes
          if (compare & 1 ||
            (!support.sortDetached && b.compareDocumentPosition(a) === compare)) {

            // Choose the first element that is related to our preferred document
            if (a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a)) {
              return -1;
            }
            if (b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b)) {
              return 1;
            }

            // Maintain original order
            return sortInput ?
              (indexOf.call(sortInput, a) - indexOf.call(sortInput, b)) :
              0;
          }

          return compare & 4 ? -1 : 1;
        } :
        function (a, b) {
          // Exit early if the nodes are identical
          if (a === b) {
            hasDuplicate = true;
            return 0;
          }

          var cur,
            i = 0,
            aup = a.parentNode,
            bup = b.parentNode,
            ap = [a],
            bp = [b];

          // Parentless nodes are either documents or disconnected
          if (!aup || !bup) {
            return a === doc ? -1 :
              b === doc ? 1 :
                aup ? -1 :
                  bup ? 1 :
                    sortInput ?
                      (indexOf.call(sortInput, a) - indexOf.call(sortInput, b)) :
                      0;

            // If the nodes are siblings, we can do a quick check
          } else if (aup === bup) {
            return siblingCheck(a, b);
          }

          // Otherwise we need full lists of their ancestors for comparison
          cur = a;
          while ((cur = cur.parentNode)) {
            ap.unshift(cur);
          }
          cur = b;
          while ((cur = cur.parentNode)) {
            bp.unshift(cur);
          }

          // Walk down the tree looking for a discrepancy
          while (ap[i] === bp[i]) {
            i++;
          }

          return i ?
            // Do a sibling check if the nodes have a common ancestor
            siblingCheck(ap[i], bp[i]) :

            // Otherwise nodes in our document sort first
            ap[i] === preferredDoc ? -1 :
              bp[i] === preferredDoc ? 1 :
                0;
        };

      return doc;
    };

    Sizzle.matches = function (expr, elements) {
      return Sizzle(expr, null, null, elements);
    };

    Sizzle.matchesSelector = function (elem, expr) {
      // Set document vars if needed
      if ((elem.ownerDocument || elem) !== document) {
        setDocument(elem);
      }

      // Make sure that attribute selectors are quoted
      expr = expr.replace(rattributeQuotes, "='$1']");

      if (support.matchesSelector && documentIsHTML &&
        (!rbuggyMatches || !rbuggyMatches.test(expr)) &&
        (!rbuggyQSA || !rbuggyQSA.test(expr))) {

        try {
          var ret = matches.call(elem, expr);

          // IE 9's matchesSelector returns false on disconnected nodes
          if (ret || support.disconnectedMatch ||
            // As well, disconnected nodes are said to be in a document
            // fragment in IE 9
            elem.document && elem.document.nodeType !== 11) {
            return ret;
          }
        } catch (e) { }
      }

      return Sizzle(expr, document, null, [elem]).length > 0;
    };

    Sizzle.contains = function (context, elem) {
      // Set document vars if needed
      if ((context.ownerDocument || context) !== document) {
        setDocument(context);
      }
      return contains(context, elem);
    };

    Sizzle.attr = function (elem, name) {
      // Set document vars if needed
      if ((elem.ownerDocument || elem) !== document) {
        setDocument(elem);
      }

      var fn = Expr.attrHandle[name.toLowerCase()],
        // Don't get fooled by Object.prototype properties (jQuery #13807)
        val = fn && hasOwn.call(Expr.attrHandle, name.toLowerCase()) ?
          fn(elem, name, !documentIsHTML) :
          undefined;

      return val !== undefined ?
        val :
        support.attributes || !documentIsHTML ?
          elem.getAttribute(name) :
          (val = elem.getAttributeNode(name)) && val.specified ?
            val.value :
            null;
    };

    Sizzle.error = function (msg) {
      throw new Error("Syntax error, unrecognized expression: " + msg);
    };

    /**
     * Document sorting and removing duplicates
     * @param {ArrayLike} results
     */
    Sizzle.uniqueSort = function (results) {
      var elem,
        duplicates = [],
        j = 0,
        i = 0;

      // Unless we *know* we can detect duplicates, assume their presence
      hasDuplicate = !support.detectDuplicates;
      sortInput = !support.sortStable && results.slice(0);
      results.sort(sortOrder);

      if (hasDuplicate) {
        while ((elem = results[i++])) {
          if (elem === results[i]) {
            j = duplicates.push(i);
          }
        }
        while (j--) {
          results.splice(duplicates[j], 1);
        }
      }

      // Clear input after sorting to release objects
      // See https://github.com/jquery/sizzle/pull/225
      sortInput = null;

      return results;
    };

    /**
     * Utility function for retrieving the text value of an array of DOM nodes
     * @param {Array|Element} elem
     */
    getText = Sizzle.getText = function (elem) {
      var node,
        ret = "",
        i = 0,
        nodeType = elem.nodeType;

      if (!nodeType) {
        // If no nodeType, this is expected to be an array
        while ((node = elem[i++])) {
          // Do not traverse comment nodes
          ret += getText(node);
        }
      } else if (nodeType === 1 || nodeType === 9 || nodeType === 11) {
        // Use textContent for elements
        // innerText usage removed for consistency of new lines (jQuery #11153)
        if (typeof elem.textContent === "string") {
          return elem.textContent;
        } else {
          // Traverse its children
          for (elem = elem.firstChild; elem; elem = elem.nextSibling) {
            ret += getText(elem);
          }
        }
      } else if (nodeType === 3 || nodeType === 4) {
        return elem.nodeValue;
      }
      // Do not include comment or processing instruction nodes

      return ret;
    };

    Expr = Sizzle.selectors = {

      // Can be adjusted by the user
      cacheLength: 50,

      createPseudo: markFunction,

      match: matchExpr,

      attrHandle: {},

      find: {},

      relative: {
        ">": { dir: "parentNode", first: true },
        " ": { dir: "parentNode" },
        "+": { dir: "previousSibling", first: true },
        "~": { dir: "previousSibling" }
      },

      preFilter: {
        "ATTR": function (match) {
          match[1] = match[1].replace(runescape, funescape);

          // Move the given value to match[3] whether quoted or unquoted
          match[3] = (match[3] || match[4] || match[5] || "").replace(runescape, funescape);

          if (match[2] === "~=") {
            match[3] = " " + match[3] + " ";
          }

          return match.slice(0, 4);
        },

        "CHILD": function (match) {
          /* matches from matchExpr["CHILD"]
            1 type (only|nth|...)
            2 what (child|of-type)
            3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
            4 xn-component of xn+y argument ([+-]?\d*n|)
            5 sign of xn-component
            6 x of xn-component
            7 sign of y-component
            8 y of y-component
          */
          match[1] = match[1].toLowerCase();

          if (match[1].slice(0, 3) === "nth") {
            // nth-* requires argument
            if (!match[3]) {
              Sizzle.error(match[0]);
            }

            // numeric x and y parameters for Expr.filter.CHILD
            // remember that false/true cast respectively to 0/1
            match[4] = +(match[4] ? match[5] + (match[6] || 1) : 2 * (match[3] === "even" || match[3] === "odd"));
            match[5] = +((match[7] + match[8]) || match[3] === "odd");

            // other types prohibit arguments
          } else if (match[3]) {
            Sizzle.error(match[0]);
          }

          return match;
        },

        "PSEUDO": function (match) {
          var excess,
            unquoted = !match[6] && match[2];

          if (matchExpr["CHILD"].test(match[0])) {
            return null;
          }

          // Accept quoted arguments as-is
          if (match[3]) {
            match[2] = match[4] || match[5] || "";

            // Strip excess characters from unquoted arguments
          } else if (unquoted && rpseudo.test(unquoted) &&
            // Get excess from tokenize (recursively)
            (excess = tokenize(unquoted, true)) &&
            // advance to the next closing parenthesis
            (excess = unquoted.indexOf(")", unquoted.length - excess) - unquoted.length)) {

            // excess is a negative index
            match[0] = match[0].slice(0, excess);
            match[2] = unquoted.slice(0, excess);
          }

          // Return only captures needed by the pseudo filter method (type and argument)
          return match.slice(0, 3);
        }
      },

      filter: {

        "TAG": function (nodeNameSelector) {
          var nodeName = nodeNameSelector.replace(runescape, funescape).toLowerCase();
          return nodeNameSelector === "*" ?
            function () { return true; } :
            function (elem) {
              return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
            };
        },

        "CLASS": function (className) {
          var pattern = classCache[className + " "];

          return pattern ||
            (pattern = new RegExp("(^|" + whitespace + ")" + className + "(" + whitespace + "|$)")) &&
            classCache(className, function (elem) {
              return pattern.test(typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "");
            });
        },

        "ATTR": function (name, operator, check) {
          return function (elem) {
            var result = Sizzle.attr(elem, name);

            if (result == null) {
              return operator === "!=";
            }
            if (!operator) {
              return true;
            }

            result += "";

            return operator === "=" ? result === check :
              operator === "!=" ? result !== check :
                operator === "^=" ? check && result.indexOf(check) === 0 :
                  operator === "*=" ? check && result.indexOf(check) > -1 :
                    operator === "$=" ? check && result.slice(-check.length) === check :
                      operator === "~=" ? (" " + result + " ").indexOf(check) > -1 :
                        operator === "|=" ? result === check || result.slice(0, check.length + 1) === check + "-" :
                          false;
          };
        },

        "CHILD": function (type, what, argument, first, last) {
          var simple = type.slice(0, 3) !== "nth",
            forward = type.slice(-4) !== "last",
            ofType = what === "of-type";

          return first === 1 && last === 0 ?

            // Shortcut for :nth-*(n)
            function (elem) {
              return !!elem.parentNode;
            } :

            function (elem, context, xml) {
              var cache, outerCache, node, diff, nodeIndex, start,
                dir = simple !== forward ? "nextSibling" : "previousSibling",
                parent = elem.parentNode,
                name = ofType && elem.nodeName.toLowerCase(),
                useCache = !xml && !ofType;

              if (parent) {

                // :(first|last|only)-(child|of-type)
                if (simple) {
                  while (dir) {
                    node = elem;
                    while ((node = node[dir])) {
                      if (ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1) {
                        return false;
                      }
                    }
                    // Reverse direction for :only-* (if we haven't yet done so)
                    start = dir = type === "only" && !start && "nextSibling";
                  }
                  return true;
                }

                start = [forward ? parent.firstChild : parent.lastChild];

                // non-xml :nth-child(...) stores cache data on `parent`
                if (forward && useCache) {
                  // Seek `elem` from a previously-cached index
                  outerCache = parent[expando] || (parent[expando] = {});
                  cache = outerCache[type] || [];
                  nodeIndex = cache[0] === dirruns && cache[1];
                  diff = cache[0] === dirruns && cache[2];
                  node = nodeIndex && parent.childNodes[nodeIndex];

                  while ((node = ++nodeIndex && node && node[dir] ||

                    // Fallback to seeking `elem` from the start
                    (diff = nodeIndex = 0) || start.pop())) {

                    // When found, cache indexes on `parent` and break
                    if (node.nodeType === 1 && ++diff && node === elem) {
                      outerCache[type] = [dirruns, nodeIndex, diff];
                      break;
                    }
                  }

                  // Use previously-cached element index if available
                } else if (useCache && (cache = (elem[expando] || (elem[expando] = {}))[type]) && cache[0] === dirruns) {
                  diff = cache[1];

                  // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...)
                } else {
                  // Use the same loop as above to seek `elem` from the start
                  while ((node = ++nodeIndex && node && node[dir] ||
                    (diff = nodeIndex = 0) || start.pop())) {

                    if ((ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1) && ++diff) {
                      // Cache the index of each encountered element
                      if (useCache) {
                        (node[expando] || (node[expando] = {}))[type] = [dirruns, diff];
                      }

                      if (node === elem) {
                        break;
                      }
                    }
                  }
                }

                // Incorporate the offset, then check against cycle size
                diff -= last;
                return diff === first || (diff % first === 0 && diff / first >= 0);
              }
            };
        },

        "PSEUDO": function (pseudo, argument) {
          // pseudo-class names are case-insensitive
          // http://www.w3.org/TR/selectors/#pseudo-classes
          // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
          // Remember that setFilters inherits from pseudos
          var args,
            fn = Expr.pseudos[pseudo] || Expr.setFilters[pseudo.toLowerCase()] ||
              Sizzle.error("unsupported pseudo: " + pseudo);

          // The user may use createPseudo to indicate that
          // arguments are needed to create the filter function
          // just as Sizzle does
          if (fn[expando]) {
            return fn(argument);
          }

          // But maintain support for old signatures
          if (fn.length > 1) {
            args = [pseudo, pseudo, "", argument];
            return Expr.setFilters.hasOwnProperty(pseudo.toLowerCase()) ?
              markFunction(function (seed, matches) {
                var idx,
                  matched = fn(seed, argument),
                  i = matched.length;
                while (i--) {
                  idx = indexOf.call(seed, matched[i]);
                  seed[idx] = !(matches[idx] = matched[i]);
                }
              }) :
              function (elem) {
                return fn(elem, 0, args);
              };
          }

          return fn;
        }
      },

      pseudos: {
        // Potentially complex pseudos
        "not": markFunction(function (selector) {
          // Trim the selector passed to compile
          // to avoid treating leading and trailing
          // spaces as combinators
          var input = [],
            results = [],
            matcher = compile(selector.replace(rtrim, "$1"));

          return matcher[expando] ?
            markFunction(function (seed, matches, context, xml) {
              var elem,
                unmatched = matcher(seed, null, xml, []),
                i = seed.length;

              // Match elements unmatched by `matcher`
              while (i--) {
                if ((elem = unmatched[i])) {
                  seed[i] = !(matches[i] = elem);
                }
              }
            }) :
            function (elem, context, xml) {
              input[0] = elem;
              matcher(input, null, xml, results);
              return !results.pop();
            };
        }),

        "has": markFunction(function (selector) {
          return function (elem) {
            return Sizzle(selector, elem).length > 0;
          };
        }),

        "contains": markFunction(function (text) {
          text = text.replace(runescape, funescape);
          return function (elem) {
            return (elem.textContent || elem.innerText || getText(elem)).indexOf(text) > -1;
          };
        }),

        // "Whether an element is represented by a :lang() selector
        // is based solely on the element's language value
        // being equal to the identifier C,
        // or beginning with the identifier C immediately followed by "-".
        // The matching of C against the element's language value is performed case-insensitively.
        // The identifier C does not have to be a valid language name."
        // http://www.w3.org/TR/selectors/#lang-pseudo
        "lang": markFunction(function (lang) {
          // lang value must be a valid identifier
          if (!ridentifier.test(lang || "")) {
            Sizzle.error("unsupported lang: " + lang);
          }
          lang = lang.replace(runescape, funescape).toLowerCase();
          return function (elem) {
            var elemLang;
            do {
              if ((elemLang = documentIsHTML ?
                elem.lang :
                elem.getAttribute("xml:lang") || elem.getAttribute("lang"))) {

                elemLang = elemLang.toLowerCase();
                return elemLang === lang || elemLang.indexOf(lang + "-") === 0;
              }
            } while ((elem = elem.parentNode) && elem.nodeType === 1);
            return false;
          };
        }),

        // Miscellaneous
        "target": function (elem) {
          var hash = window.location && window.location.hash;
          return hash && hash.slice(1) === elem.id;
        },

        "root": function (elem) {
          return elem === docElem;
        },

        "focus": function (elem) {
          return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
        },

        // Boolean properties
        "enabled": function (elem) {
          return elem.disabled === false;
        },

        "disabled": function (elem) {
          return elem.disabled === true;
        },

        "checked": function (elem) {
          // In CSS3, :checked should return both checked and selected elements
          // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
          var nodeName = elem.nodeName.toLowerCase();
          return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
        },

        "selected": function (elem) {
          // Accessing this property makes selected-by-default
          // options in Safari work properly
          if (elem.parentNode) {
            elem.parentNode.selectedIndex;
          }

          return elem.selected === true;
        },

        // Contents
        "empty": function (elem) {
          // http://www.w3.org/TR/selectors/#empty-pseudo
          // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),
          //   but not by others (comment: 8; processing instruction: 7; etc.)
          // nodeType < 6 works because attributes (2) do not appear as children
          for (elem = elem.firstChild; elem; elem = elem.nextSibling) {
            if (elem.nodeType < 6) {
              return false;
            }
          }
          return true;
        },

        "parent": function (elem) {
          return !Expr.pseudos["empty"](elem);
        },

        // Element/input types
        "header": function (elem) {
          return rheader.test(elem.nodeName);
        },

        "input": function (elem) {
          return rinputs.test(elem.nodeName);
        },

        "button": function (elem) {
          var name = elem.nodeName.toLowerCase();
          return name === "input" && elem.type === "button" || name === "button";
        },

        "text": function (elem) {
          var attr;
          return elem.nodeName.toLowerCase() === "input" &&
            elem.type === "text" &&

            // Support: IE<8
            // New HTML5 attribute values (e.g., "search") appear with elem.type === "text"
            ((attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text");
        },

        // Position-in-collection
        "first": createPositionalPseudo(function () {
          return [0];
        }),

        "last": createPositionalPseudo(function (matchIndexes, length) {
          return [length - 1];
        }),

        "eq": createPositionalPseudo(function (matchIndexes, length, argument) {
          return [argument < 0 ? argument + length : argument];
        }),

        "even": createPositionalPseudo(function (matchIndexes, length) {
          var i = 0;
          for (; i < length; i += 2) {
            matchIndexes.push(i);
          }
          return matchIndexes;
        }),

        "odd": createPositionalPseudo(function (matchIndexes, length) {
          var i = 1;
          for (; i < length; i += 2) {
            matchIndexes.push(i);
          }
          return matchIndexes;
        }),

        "lt": createPositionalPseudo(function (matchIndexes, length, argument) {
          var i = argument < 0 ? argument + length : argument;
          for (; --i >= 0;) {
            matchIndexes.push(i);
          }
          return matchIndexes;
        }),

        "gt": createPositionalPseudo(function (matchIndexes, length, argument) {
          var i = argument < 0 ? argument + length : argument;
          for (; ++i < length;) {
            matchIndexes.push(i);
          }
          return matchIndexes;
        })
      }
    };

    Expr.pseudos["nth"] = Expr.pseudos["eq"];

    // Add button/input type pseudos
    for (i in { radio: true, checkbox: true, file: true, password: true, image: true }) {
      Expr.pseudos[i] = createInputPseudo(i);
    }
    for (i in { submit: true, reset: true }) {
      Expr.pseudos[i] = createButtonPseudo(i);
    }

    // Easy API for creating new setFilters
    function setFilters() { }
    setFilters.prototype = Expr.filters = Expr.pseudos;
    Expr.setFilters = new setFilters();

    tokenize = Sizzle.tokenize = function (selector, parseOnly) {
      var matched, match, tokens, type,
        soFar, groups, preFilters,
        cached = tokenCache[selector + " "];

      if (cached) {
        return parseOnly ? 0 : cached.slice(0);
      }

      soFar = selector;
      groups = [];
      preFilters = Expr.preFilter;

      while (soFar) {

        // Comma and first run
        if (!matched || (match = rcomma.exec(soFar))) {
          if (match) {
            // Don't consume trailing commas as valid
            soFar = soFar.slice(match[0].length) || soFar;
          }
          groups.push((tokens = []));
        }

        matched = false;

        // Combinators
        if ((match = rcombinators.exec(soFar))) {
          matched = match.shift();
          tokens.push({
            value: matched,
            // Cast descendant combinators to space
            type: match[0].replace(rtrim, " ")
          });
          soFar = soFar.slice(matched.length);
        }

        // Filters
        for (type in Expr.filter) {
          if ((match = matchExpr[type].exec(soFar)) && (!preFilters[type] ||
            (match = preFilters[type](match)))) {
            matched = match.shift();
            tokens.push({
              value: matched,
              type: type,
              matches: match
            });
            soFar = soFar.slice(matched.length);
          }
        }

        if (!matched) {
          break;
        }
      }

      // Return the length of the invalid excess
      // if we're just parsing
      // Otherwise, throw an error or return tokens
      return parseOnly ?
        soFar.length :
        soFar ?
          Sizzle.error(selector) :
          // Cache the tokens
          tokenCache(selector, groups).slice(0);
    };

    function toSelector(tokens) {
      var i = 0,
        len = tokens.length,
        selector = "";
      for (; i < len; i++) {
        selector += tokens[i].value;
      }
      return selector;
    }

    function addCombinator(matcher, combinator, base) {
      var dir = combinator.dir,
        checkNonElements = base && dir === "parentNode",
        doneName = done++;

      return combinator.first ?
        // Check against closest ancestor/preceding element
        function (elem, context, xml) {
          while ((elem = elem[dir])) {
            if (elem.nodeType === 1 || checkNonElements) {
              return matcher(elem, context, xml);
            }
          }
        } :

        // Check against all ancestor/preceding elements
        function (elem, context, xml) {
          var oldCache, outerCache,
            newCache = [dirruns, doneName];

          // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching
          if (xml) {
            while ((elem = elem[dir])) {
              if (elem.nodeType === 1 || checkNonElements) {
                if (matcher(elem, context, xml)) {
                  return true;
                }
              }
            }
          } else {
            while ((elem = elem[dir])) {
              if (elem.nodeType === 1 || checkNonElements) {
                outerCache = elem[expando] || (elem[expando] = {});
                if ((oldCache = outerCache[dir]) &&
                  oldCache[0] === dirruns && oldCache[1] === doneName) {

                  // Assign to newCache so results back-propagate to previous elements
                  return (newCache[2] = oldCache[2]);
                } else {
                  // Reuse newcache so results back-propagate to previous elements
                  outerCache[dir] = newCache;

                  // A match means we're done; a fail means we have to keep checking
                  if ((newCache[2] = matcher(elem, context, xml))) {
                    return true;
                  }
                }
              }
            }
          }
        };
    }

    function elementMatcher(matchers) {
      return matchers.length > 1 ?
        function (elem, context, xml) {
          var i = matchers.length;
          while (i--) {
            if (!matchers[i](elem, context, xml)) {
              return false;
            }
          }
          return true;
        } :
        matchers[0];
    }

    function multipleContexts(selector, contexts, results) {
      var i = 0,
        len = contexts.length;
      for (; i < len; i++) {
        Sizzle(selector, contexts[i], results);
      }
      return results;
    }

    function condense(unmatched, map, filter, context, xml) {
      var elem,
        newUnmatched = [],
        i = 0,
        len = unmatched.length,
        mapped = map != null;

      for (; i < len; i++) {
        if ((elem = unmatched[i])) {
          if (!filter || filter(elem, context, xml)) {
            newUnmatched.push(elem);
            if (mapped) {
              map.push(i);
            }
          }
        }
      }

      return newUnmatched;
    }

    function setMatcher(preFilter, selector, matcher, postFilter, postFinder, postSelector) {
      if (postFilter && !postFilter[expando]) {
        postFilter = setMatcher(postFilter);
      }
      if (postFinder && !postFinder[expando]) {
        postFinder = setMatcher(postFinder, postSelector);
      }
      return markFunction(function (seed, results, context, xml) {
        var temp, i, elem,
          preMap = [],
          postMap = [],
          preexisting = results.length,

          // Get initial elements from seed or context
          elems = seed || multipleContexts(selector || "*", context.nodeType ? [context] : context, []),

          // Prefilter to get matcher input, preserving a map for seed-results synchronization
          matcherIn = preFilter && (seed || !selector) ?
            condense(elems, preMap, preFilter, context, xml) :
            elems,

          matcherOut = matcher ?
            // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
            postFinder || (seed ? preFilter : preexisting || postFilter) ?

              // ...intermediate processing is necessary
              [] :

              // ...otherwise use results directly
              results :
            matcherIn;

        // Find primary matches
        if (matcher) {
          matcher(matcherIn, matcherOut, context, xml);
        }

        // Apply postFilter
        if (postFilter) {
          temp = condense(matcherOut, postMap);
          postFilter(temp, [], context, xml);

          // Un-match failing elements by moving them back to matcherIn
          i = temp.length;
          while (i--) {
            if ((elem = temp[i])) {
              matcherOut[postMap[i]] = !(matcherIn[postMap[i]] = elem);
            }
          }
        }

        if (seed) {
          if (postFinder || preFilter) {
            if (postFinder) {
              // Get the final matcherOut by condensing this intermediate into postFinder contexts
              temp = [];
              i = matcherOut.length;
              while (i--) {
                if ((elem = matcherOut[i])) {
                  // Restore matcherIn since elem is not yet a final match
                  temp.push((matcherIn[i] = elem));
                }
              }
              postFinder(null, (matcherOut = []), temp, xml);
            }

            // Move matched elements from seed to results to keep them synchronized
            i = matcherOut.length;
            while (i--) {
              if ((elem = matcherOut[i]) &&
                (temp = postFinder ? indexOf.call(seed, elem) : preMap[i]) > -1) {

                seed[temp] = !(results[temp] = elem);
              }
            }
          }

          // Add elements to results, through postFinder if defined
        } else {
          matcherOut = condense(
            matcherOut === results ?
              matcherOut.splice(preexisting, matcherOut.length) :
              matcherOut
          );
          if (postFinder) {
            postFinder(null, results, matcherOut, xml);
          } else {
            push.apply(results, matcherOut);
          }
        }
      });
    }

    function matcherFromTokens(tokens) {
      var checkContext, matcher, j,
        len = tokens.length,
        leadingRelative = Expr.relative[tokens[0].type],
        implicitRelative = leadingRelative || Expr.relative[" "],
        i = leadingRelative ? 1 : 0,

        // The foundational matcher ensures that elements are reachable from top-level context(s)
        matchContext = addCombinator(function (elem) {
          return elem === checkContext;
        }, implicitRelative, true),
        matchAnyContext = addCombinator(function (elem) {
          return indexOf.call(checkContext, elem) > -1;
        }, implicitRelative, true),
        matchers = [function (elem, context, xml) {
          return (!leadingRelative && (xml || context !== outermostContext)) || (
            (checkContext = context).nodeType ?
              matchContext(elem, context, xml) :
              matchAnyContext(elem, context, xml));
        }];

      for (; i < len; i++) {
        if ((matcher = Expr.relative[tokens[i].type])) {
          matchers = [addCombinator(elementMatcher(matchers), matcher)];
        } else {
          matcher = Expr.filter[tokens[i].type].apply(null, tokens[i].matches);

          // Return special upon seeing a positional matcher
          if (matcher[expando]) {
            // Find the next relative operator (if any) for proper handling
            j = ++i;
            for (; j < len; j++) {
              if (Expr.relative[tokens[j].type]) {
                break;
              }
            }
            return setMatcher(
              i > 1 && elementMatcher(matchers),
              i > 1 && toSelector(
                // If the preceding token was a descendant combinator, insert an implicit any-element `*`
                tokens.slice(0, i - 1).concat({ value: tokens[i - 2].type === " " ? "*" : "" })
              ).replace(rtrim, "$1"),
              matcher,
              i < j && matcherFromTokens(tokens.slice(i, j)),
              j < len && matcherFromTokens((tokens = tokens.slice(j))),
              j < len && toSelector(tokens)
            );
          }
          matchers.push(matcher);
        }
      }

      return elementMatcher(matchers);
    }

    function matcherFromGroupMatchers(elementMatchers, setMatchers) {
      var bySet = setMatchers.length > 0,
        byElement = elementMatchers.length > 0,
        superMatcher = function (seed, context, xml, results, outermost) {
          var elem, j, matcher,
            matchedCount = 0,
            i = "0",
            unmatched = seed && [],
            setMatched = [],
            contextBackup = outermostContext,
            // We must always have either seed elements or outermost context
            elems = seed || byElement && Expr.find["TAG"]("*", outermost),
            // Use integer dirruns iff this is the outermost matcher
            dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),
            len = elems.length;

          if (outermost) {
            outermostContext = context !== document && context;
          }

          // Add elements passing elementMatchers directly to results
          // Keep `i` a string if there are no elements so `matchedCount` will be "00" below
          // Support: IE<9, Safari
          // Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id
          for (; i !== len && (elem = elems[i]) != null; i++) {
            if (byElement && elem) {
              j = 0;
              while ((matcher = elementMatchers[j++])) {
                if (matcher(elem, context, xml)) {
                  results.push(elem);
                  break;
                }
              }
              if (outermost) {
                dirruns = dirrunsUnique;
              }
            }

            // Track unmatched elements for set filters
            if (bySet) {
              // They will have gone through all possible matchers
              if ((elem = !matcher && elem)) {
                matchedCount--;
              }

              // Lengthen the array for every element, matched or not
              if (seed) {
                unmatched.push(elem);
              }
            }
          }

          // Apply set filters to unmatched elements
          matchedCount += i;
          if (bySet && i !== matchedCount) {
            j = 0;
            while ((matcher = setMatchers[j++])) {
              matcher(unmatched, setMatched, context, xml);
            }

            if (seed) {
              // Reintegrate element matches to eliminate the need for sorting
              if (matchedCount > 0) {
                while (i--) {
                  if (!(unmatched[i] || setMatched[i])) {
                    setMatched[i] = pop.call(results);
                  }
                }
              }

              // Discard index placeholder values to get only actual matches
              setMatched = condense(setMatched);
            }

            // Add matches to results
            push.apply(results, setMatched);

            // Seedless set matches succeeding multiple successful matchers stipulate sorting
            if (outermost && !seed && setMatched.length > 0 &&
              (matchedCount + setMatchers.length) > 1) {

              Sizzle.uniqueSort(results);
            }
          }

          // Override manipulation of globals by nested matchers
          if (outermost) {
            dirruns = dirrunsUnique;
            outermostContext = contextBackup;
          }

          return unmatched;
        };

      return bySet ?
        markFunction(superMatcher) :
        superMatcher;
    }

    compile = Sizzle.compile = function (selector, match /* Internal Use Only */) {
      var i,
        setMatchers = [],
        elementMatchers = [],
        cached = compilerCache[selector + " "];

      if (!cached) {
        // Generate a function of recursive functions that can be used to check each element
        if (!match) {
          match = tokenize(selector);
        }
        i = match.length;
        while (i--) {
          cached = matcherFromTokens(match[i]);
          if (cached[expando]) {
            setMatchers.push(cached);
          } else {
            elementMatchers.push(cached);
          }
        }

        // Cache the compiled function
        cached = compilerCache(selector, matcherFromGroupMatchers(elementMatchers, setMatchers));

        // Save selector and tokenization
        cached.selector = selector;
      }
      return cached;
    };

    /**
     * A low-level selection function that works with Sizzle's compiled
     *  selector functions
     * @param {String|Function} selector A selector or a pre-compiled
     *  selector function built with Sizzle.compile
     * @param {Element} context
     * @param {Array} [results]
     * @param {Array} [seed] A set of elements to match against
     */
    select = Sizzle.select = function (selector, context, results, seed) {
      var i, tokens, token, type, find,
        compiled = typeof selector === "function" && selector,
        match = !seed && tokenize((selector = compiled.selector || selector));

      results = results || [];

      // Try to minimize operations if there is no seed and only one group
      if (match.length === 1) {

        // Take a shortcut and set the context if the root selector is an ID
        tokens = match[0] = match[0].slice(0);
        if (tokens.length > 2 && (token = tokens[0]).type === "ID" &&
          support.getById && context.nodeType === 9 && documentIsHTML &&
          Expr.relative[tokens[1].type]) {

          context = (Expr.find["ID"](token.matches[0].replace(runescape, funescape), context) || [])[0];
          if (!context) {
            return results;

            // Precompiled matchers will still verify ancestry, so step up a level
          } else if (compiled) {
            context = context.parentNode;
          }

          selector = selector.slice(tokens.shift().value.length);
        }

        // Fetch a seed set for right-to-left matching
        i = matchExpr["needsContext"].test(selector) ? 0 : tokens.length;
        while (i--) {
          token = tokens[i];

          // Abort if we hit a combinator
          if (Expr.relative[(type = token.type)]) {
            break;
          }
          if ((find = Expr.find[type])) {
            // Search, expanding context for leading sibling combinators
            if ((seed = find(
              token.matches[0].replace(runescape, funescape),
              rsibling.test(tokens[0].type) && testContext(context.parentNode) || context
            ))) {

              // If seed is empty or no tokens remain, we can return early
              tokens.splice(i, 1);
              selector = seed.length && toSelector(tokens);
              if (!selector) {
                push.apply(results, seed);
                return results;
              }

              break;
            }
          }
        }
      }

      // Compile and execute a filtering function if one is not provided
      // Provide `match` to avoid retokenization if we modified the selector above
      (compiled || compile(selector, match))(
        seed,
        context,
        !documentIsHTML,
        results,
        rsibling.test(selector) && testContext(context.parentNode) || context
      );
      return results;
    };

    // One-time assignments

    // Sort stability
    support.sortStable = expando.split("").sort(sortOrder).join("") === expando;

    // Support: Chrome 14-35+
    // Always assume duplicates if they aren't passed to the comparison function
    support.detectDuplicates = !!hasDuplicate;

    // Initialize against the default document
    setDocument();

    // Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
    // Detached nodes confoundingly follow *each other*
    support.sortDetached = true;

    // Support: IE<8
    // Prevent attribute/property "interpolation"
    // http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
    /*if (!assert(function (div) {
      div.innerHTML = "<a href='#'></a>";
      return div.firstChild.getAttribute("href") === "#";
    })) {
      addHandle("type|href|height|width", function (elem, name, isXML) {
        if (!isXML) {
          return elem.getAttribute(name, name.toLowerCase() === "type" ? 1 : 2);
        }
      });
    }*/

    // Support: IE<9
    // Use defaultValue in place of getAttribute("value")
    /*if (!support.attributes || !assert(function (div) {
      div.innerHTML = "<input/>";
      div.firstChild.setAttribute("value", "");
      return div.firstChild.getAttribute("value") === "";
    })) {
      addHandle("value", function (elem, name, isXML) {
        if (!isXML && elem.nodeName.toLowerCase() === "input") {
          return elem.defaultValue;
        }
      });
    }*/

    // Support: IE<9
    // Use getAttributeNode to fetch booleans when getAttribute lies
    /*if (!assert(function (div) {
      return div.getAttribute("disabled") == null;
    })) {
      addHandle(booleans, function (elem, name, isXML) {
        var val;
        if (!isXML) {
          return elem[name] === true ? name.toLowerCase() :
            (val = elem.getAttributeNode(name)) && val.specified ?
              val.value :
              null;
        }
      });
    }*/

    // EXPOSE
    return Sizzle;
  }
);

/*eslint-enable */

/**
 * Arr.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
 */

/**
 * Array utility class.
 *
 * @private
 * @class tinymce.util.Arr
 */
define(
  'tinymce.core.util.Arr',
  [
  ],
  function () {
    var isArray = Array.isArray || function (obj) {
      return Object.prototype.toString.call(obj) === "[object Array]";
    };

    var toArray = function (obj) {
      var array = obj, i, l;

      if (!isArray(obj)) {
        array = [];
        for (i = 0, l = obj.length; i < l; i++) {
          array[i] = obj[i];
        }
      }

      return array;
    };

    var each = function (o, cb, s) {
      var n, l;

      if (!o) {
        return 0;
      }

      s = s || o;

      if (o.length !== undefined) {
        // Indexed arrays, needed for Safari
        for (n = 0, l = o.length; n < l; n++) {
          if (cb.call(s, o[n], n, o) === false) {
            return 0;
          }
        }
      } else {
        // Hashtables
        for (n in o) {
          if (o.hasOwnProperty(n)) {
            if (cb.call(s, o[n], n, o) === false) {
              return 0;
            }
          }
        }
      }

      return 1;
    };

    var map = function (array, callback) {
      var out = [];

      each(array, function (item, index) {
        out.push(callback(item, index, array));
      });

      return out;
    };

    var filter = function (a, f) {
      var o = [];

      each(a, function (v, index) {
        if (!f || f(v, index, a)) {
          o.push(v);
        }
      });

      return o;
    };

    var indexOf = function (a, v) {
      var i, l;

      if (a) {
        for (i = 0, l = a.length; i < l; i++) {
          if (a[i] === v) {
            return i;
          }
        }
      }

      return -1;
    };

    var reduce = function (collection, iteratee, accumulator, thisArg) {
      var i = 0;

      if (arguments.length < 3) {
        accumulator = collection[0];
      }

      for (; i < collection.length; i++) {
        accumulator = iteratee.call(thisArg, accumulator, collection[i], i);
      }

      return accumulator;
    };

    var findIndex = function (array, predicate, thisArg) {
      var i, l;

      for (i = 0, l = array.length; i < l; i++) {
        if (predicate.call(thisArg, array[i], i, array)) {
          return i;
        }
      }

      return -1;
    };

    var find = function (array, predicate, thisArg) {
      var idx = findIndex(array, predicate, thisArg);

      if (idx !== -1) {
        return array[idx];
      }

      return undefined;
    };

    var last = function (collection) {
      return collection[collection.length - 1];
    };

    return {
      isArray: isArray,
      toArray: toArray,
      each: each,
      map: map,
      filter: filter,
      indexOf: indexOf,
      reduce: reduce,
      findIndex: findIndex,
      find: find,
      last: last
    };
  }
);
/**
 * Tools.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
 */

/**
 * This class contains various utlity functions. These are also exposed
 * directly on the tinymce namespace.
 *
 * @class tinymce.util.Tools
 */
define(
  'tinymce.core.util.Tools',
  [
    'global!window',
    'tinymce.core.Env',
    'tinymce.core.util.Arr'
  ],
  function (window, Env, Arr) {
    /**
     * Removes whitespace from the beginning and end of a string.
     *
     * @method trim
     * @param {String} s String to remove whitespace from.
     * @return {String} New string with removed whitespace.
     */
    var whiteSpaceRegExp = /^\s*|\s*$/g;

    var trim = function (str) {
      return (str === null || str === undefined) ? '' : ("" + str).replace(whiteSpaceRegExp, '');
    };

    /**
     * Checks if a object is of a specific type for example an array.
     *
     * @method is
     * @param {Object} obj Object to check type of.
     * @param {string} type Optional type to check for.
     * @return {Boolean} true/false if the object is of the specified type.
     */
    var is = function (obj, type) {
      if (!type) {
        return obj !== undefined;
      }

      if (type == 'array' && Arr.isArray(obj)) {
        return true;
      }

      return typeof obj == type;
    };

    /**
     * Makes a name/object map out of an array with names.
     *
     * @method makeMap
     * @param {Array/String} items Items to make map out of.
     * @param {String} delim Optional delimiter to split string by.
     * @param {Object} map Optional map to add items to.
     * @return {Object} Name/value map of items.
     */
    var makeMap = function (items, delim, map) {
      var i;

      items = items || [];
      delim = delim || ',';

      if (typeof items == "string") {
        items = items.split(delim);
      }

      map = map || {};

      i = items.length;
      while (i--) {
        map[items[i]] = {};
      }

      return map;
    };

    /**
     * JavaScript does not protect hasOwnProperty method, so it is possible to overwrite it. This is
     * object independent version.
     *
     * @param {Object} obj
     * @param {String} prop
     * @returns {Boolean}
     */
    var hasOwnProperty = function (obj, prop) {
      return Object.prototype.hasOwnProperty.call(obj, prop);
    };

    /**
     * Creates a class, subclass or static singleton.
     * More details on this method can be found in the Wiki.
     *
     * @method create
     * @param {String} s Class name, inheritance and prefix.
     * @param {Object} p Collection of methods to add to the class.
     * @param {Object} root Optional root object defaults to the global window object.
     * @example
     * // Creates a basic class
     * tinymce.create('tinymce.somepackage.SomeClass', {
     *     SomeClass: function() {
     *         // Class constructor
     *     },
     *
     *     method: function() {
     *         // Some method
     *     }
     * });
     *
     * // Creates a basic subclass class
     * tinymce.create('tinymce.somepackage.SomeSubClass:tinymce.somepackage.SomeClass', {
     *     SomeSubClass: function() {
     *         // Class constructor
     *         this.parent(); // Call parent constructor
     *     },
     *
     *     method: function() {
     *         // Some method
     *         this.parent(); // Call parent method
     *     },
     *
     *     'static': {
     *         staticMethod: function() {
     *             // Static method
     *         }
     *     }
     * });
     *
     * // Creates a singleton/static class
     * tinymce.create('static tinymce.somepackage.SomeSingletonClass', {
     *     method: function() {
     *         // Some method
     *     }
     * });
     */
    var create = function (s, p, root) {
      var self = this, sp, ns, cn, scn, c, de = 0;

      // Parse : <prefix> <class>:<super class>
      s = /^((static) )?([\w.]+)(:([\w.]+))?/.exec(s);
      cn = s[3].match(/(^|\.)(\w+)$/i)[2]; // Class name

      // Create namespace for new class
      ns = self.createNS(s[3].replace(/\.\w+$/, ''), root);

      // Class already exists
      if (ns[cn]) {
        return;
      }

      // Make pure static class
      if (s[2] == 'static') {
        ns[cn] = p;

        if (this.onCreate) {
          this.onCreate(s[2], s[3], ns[cn]);
        }

        return;
      }

      // Create default constructor
      if (!p[cn]) {
        p[cn] = function () { };
        de = 1;
      }

      // Add constructor and methods
      ns[cn] = p[cn];
      self.extend(ns[cn].prototype, p);

      // Extend
      if (s[5]) {
        sp = self.resolve(s[5]).prototype;
        scn = s[5].match(/\.(\w+)$/i)[1]; // Class name

        // Extend constructor
        c = ns[cn];
        if (de) {
          // Add passthrough constructor
          ns[cn] = function () {
            return sp[scn].apply(this, arguments);
          };
        } else {
          // Add inherit constructor
          ns[cn] = function () {
            this.parent = sp[scn];
            return c.apply(this, arguments);
          };
        }
        ns[cn].prototype[cn] = ns[cn];

        // Add super methods
        self.each(sp, function (f, n) {
          ns[cn].prototype[n] = sp[n];
        });

        // Add overridden methods
        self.each(p, function (f, n) {
          // Extend methods if needed
          if (sp[n]) {
            ns[cn].prototype[n] = function () {
              this.parent = sp[n];
              return f.apply(this, arguments);
            };
          } else {
            if (n != cn) {
              ns[cn].prototype[n] = f;
            }
          }
        });
      }

      // Add static methods
      /*jshint sub:true*/
      /*eslint dot-notation:0*/
      self.each(p['static'], function (f, n) {
        ns[cn][n] = f;
      });
    };

    var extend = function (obj, ext) {
      var i, l, name, args = arguments, value;

      for (i = 1, l = args.length; i < l; i++) {
        ext = args[i];
        for (name in ext) {
          if (ext.hasOwnProperty(name)) {
            value = ext[name];

            if (value !== undefined) {
              obj[name] = value;
            }
          }
        }
      }

      return obj;
    };

    /**
     * Executed the specified function for each item in a object tree.
     *
     * @method walk
     * @param {Object} o Object tree to walk though.
     * @param {function} f Function to call for each item.
     * @param {String} n Optional name of collection inside the objects to walk for example childNodes.
     * @param {String} s Optional scope to execute the function in.
     */
    var walk = function (o, f, n, s) {
      s = s || this;

      if (o) {
        if (n) {
          o = o[n];
        }

        Arr.each(o, function (o, i) {
          if (f.call(s, o, i, n) === false) {
            return false;
          }

          walk(o, f, n, s);
        });
      }
    };

    /**
     * Creates a namespace on a specific object.
     *
     * @method createNS
     * @param {String} n Namespace to create for example a.b.c.d.
     * @param {Object} o Optional object to add namespace to, defaults to window.
     * @return {Object} New namespace object the last item in path.
     * @example
     * // Create some namespace
     * tinymce.createNS('tinymce.somepackage.subpackage');
     *
     * // Add a singleton
     * var tinymce.somepackage.subpackage.SomeSingleton = {
     *     method: function() {
     *         // Some method
     *     }
     * };
     */
    var createNS = function (n, o) {
      var i, v;

      o = o || window;

      n = n.split('.');
      for (i = 0; i < n.length; i++) {
        v = n[i];

        if (!o[v]) {
          o[v] = {};
        }

        o = o[v];
      }

      return o;
    };

    /**
     * Resolves a string and returns the object from a specific structure.
     *
     * @method resolve
     * @param {String} n Path to resolve for example a.b.c.d.
     * @param {Object} o Optional object to search though, defaults to window.
     * @return {Object} Last object in path or null if it couldn't be resolved.
     * @example
     * // Resolve a path into an object reference
     * var obj = tinymce.resolve('a.b.c.d');
     */
    var resolve = function (n, o) {
      var i, l;

      o = o || window;

      n = n.split('.');
      for (i = 0, l = n.length; i < l; i++) {
        o = o[n[i]];

        if (!o) {
          break;
        }
      }

      return o;
    };

    /**
     * Splits a string but removes the whitespace before and after each value.
     *
     * @method explode
     * @param {string} s String to split.
     * @param {string} d Delimiter to split by.
     * @example
     * // Split a string into an array with a,b,c
     * var arr = tinymce.explode('a, b,   c');
     */
    var explode = function (s, d) {
      if (!s || is(s, 'array')) {
        return s;
      }

      return Arr.map(s.split(d || ','), trim);
    };

    var _addCacheSuffix = function (url) {
      var cacheSuffix = Env.cacheSuffix;

      if (cacheSuffix) {
        url += (url.indexOf('?') === -1 ? '?' : '&') + cacheSuffix;
      }

      return url;
    };

    return {
      trim: trim,

      /**
       * Returns true/false if the object is an array or not.
       *
       * @method isArray
       * @param {Object} obj Object to check.
       * @return {boolean} true/false state if the object is an array or not.
       */
      isArray: Arr.isArray,

      is: is,

      /**
       * Converts the specified object into a real JavaScript array.
       *
       * @method toArray
       * @param {Object} obj Object to convert into array.
       * @return {Array} Array object based in input.
       */
      toArray: Arr.toArray,
      makeMap: makeMap,

      /**
       * Performs an iteration of all items in a collection such as an object or array. This method will execure the
       * callback function for each item in the collection, if the callback returns false the iteration will terminate.
       * The callback has the following format: cb(value, key_or_index).
       *
       * @method each
       * @param {Object} o Collection to iterate.
       * @param {function} cb Callback function to execute for each item.
       * @param {Object} s Optional scope to execute the callback in.
       * @example
       * // Iterate an array
       * tinymce.each([1,2,3], function(v, i) {
       *     console.debug("Value: " + v + ", Index: " + i);
       * });
       *
       * // Iterate an object
       * tinymce.each({a: 1, b: 2, c: 3], function(v, k) {
       *     console.debug("Value: " + v + ", Key: " + k);
       * });
       */
      each: Arr.each,

      /**
       * Creates a new array by the return value of each iteration function call. This enables you to convert
       * one array list into another.
       *
       * @method map
       * @param {Array} array Array of items to iterate.
       * @param {function} callback Function to call for each item. It's return value will be the new value.
       * @return {Array} Array with new values based on function return values.
       */
      map: Arr.map,

      /**
       * Filters out items from the input array by calling the specified function for each item.
       * If the function returns false the item will be excluded if it returns true it will be included.
       *
       * @method grep
       * @param {Array} a Array of items to loop though.
       * @param {function} f Function to call for each item. Include/exclude depends on it's return value.
       * @return {Array} New array with values imported and filtered based in input.
       * @example
       * // Filter out some items, this will return an array with 4 and 5
       * var items = tinymce.grep([1,2,3,4,5], function(v) {return v > 3;});
       */
      grep: Arr.filter,

      /**
       * Returns an index of the item or -1 if item is not present in the array.
       *
       * @method inArray
       * @param {any} item Item to search for.
       * @param {Array} arr Array to search in.
       * @return {Number} index of the item or -1 if item was not found.
       */
      inArray: Arr.indexOf,

      hasOwn: hasOwnProperty,

      extend: extend,
      create: create,
      walk: walk,
      createNS: createNS,
      resolve: resolve,
      explode: explode,
      _addCacheSuffix: _addCacheSuffix
    };
  }
);
/**
 * DomQuery.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
 */

/**
 * This class mimics most of the jQuery API:
 *
 * This is whats currently implemented:
 * - Utility functions
 * - DOM traversial
 * - DOM manipulation
 * - Event binding
 *
 * This is not currently implemented:
 * - Dimension
 * - Ajax
 * - Animation
 * - Advanced chaining
 *
 * @example
 * var $ = tinymce.dom.DomQuery;
 * $('p').attr('attr', 'value').addClass('class');
 *
 * @class tinymce.dom.DomQuery
 */
define(
  'tinymce.core.dom.DomQuery',
  [
    'global!document',
    'tinymce.core.dom.EventUtils',
    'tinymce.core.dom.Sizzle',
    'tinymce.core.Env',
    'tinymce.core.util.Tools'
  ],
  function (document, EventUtils, Sizzle, Env, Tools) {
    var doc = document, push = Array.prototype.push, slice = Array.prototype.slice;
    var rquickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/;
    var Event = EventUtils.Event, undef;
    var skipUniques = Tools.makeMap('children,contents,next,prev');

    var isDefined = function (obj) {
      return typeof obj !== 'undefined';
    };

    var isString = function (obj) {
      return typeof obj === 'string';
    };

    var isWindow = function (obj) {
      return obj && obj == obj.window;
    };

    var createFragment = function (html, fragDoc) {
      var frag, node, container;

      fragDoc = fragDoc || doc;
      container = fragDoc.createElement('div');
      frag = fragDoc.createDocumentFragment();
      container.innerHTML = html;

      while ((node = container.firstChild)) {
        frag.appendChild(node);
      }

      return frag;
    };

    var domManipulate = function (targetNodes, sourceItem, callback, reverse) {
      var i;

      if (isString(sourceItem)) {
        sourceItem = createFragment(sourceItem, getElementDocument(targetNodes[0]));
      } else if (sourceItem.length && !sourceItem.nodeType) {
        sourceItem = DomQuery.makeArray(sourceItem);

        if (reverse) {
          for (i = sourceItem.length - 1; i >= 0; i--) {
            domManipulate(targetNodes, sourceItem[i], callback, reverse);
          }
        } else {
          for (i = 0; i < sourceItem.length; i++) {
            domManipulate(targetNodes, sourceItem[i], callback, reverse);
          }
        }

        return targetNodes;
      }

      if (sourceItem.nodeType) {
        i = targetNodes.length;
        while (i--) {
          callback.call(targetNodes[i], sourceItem);
        }
      }

      return targetNodes;
    };

    var hasClass = function (node, className) {
      return node && className && (' ' + node.className + ' ').indexOf(' ' + className + ' ') !== -1;
    };

    var wrap = function (elements, wrapper, all) {
      var lastParent, newWrapper;

      wrapper = DomQuery(wrapper)[0];

      elements.each(function () {
        var self = this;

        if (!all || lastParent != self.parentNode) {
          lastParent = self.parentNode;
          newWrapper = wrapper.cloneNode(false);
          self.parentNode.insertBefore(newWrapper, self);
          newWrapper.appendChild(self);
        } else {
          newWrapper.appendChild(self);
        }
      });

      return elements;
    };

    var numericCssMap = Tools.makeMap('fillOpacity fontWeight lineHeight opacity orphans widows zIndex zoom', ' ');
    var booleanMap = Tools.makeMap('checked compact declare defer disabled ismap multiple nohref noshade nowrap readonly selected', ' ');
    var propFix = {
      'for': 'htmlFor',
      'class': 'className',
      'readonly': 'readOnly'
    };
    var cssFix = {
      'float': 'cssFloat'
    };

    var attrHooks = {}, cssHooks = {};

    var DomQuery = function (selector, context) {
      /*eslint new-cap:0 */
      return new DomQuery.fn.init(selector, context);
    };

    var inArray = function (item, array) {
      var i;

      if (array.indexOf) {
        return array.indexOf(item);
      }

      i = array.length;
      while (i--) {
        if (array[i] === item) {
          return i;
        }
      }

      return -1;
    };

    var whiteSpaceRegExp = /^\s*|\s*$/g;

    var trim = function (str) {
      return (str === null || str === undef) ? '' : ("" + str).replace(whiteSpaceRegExp, '');
    };

    var each = function (obj, callback) {
      var length, key, i, undef, value;

      if (obj) {
        length = obj.length;

        if (length === undef) {
          // Loop object items
          for (key in obj) {
            if (obj.hasOwnProperty(key)) {
              value = obj[key];
              if (callback.call(value, key, value) === false) {
                break;
              }
            }
          }
        } else {
          // Loop array items
          for (i = 0; i < length; i++) {
            value = obj[i];
            if (callback.call(value, i, value) === false) {
              break;
            }
          }
        }
      }

      return obj;
    };

    var grep = function (array, callback) {
      var out = [];

      each(array, function (i, item) {
        if (callback(item, i)) {
          out.push(item);
        }
      });

      return out;
    };

    var getElementDocument = function (element) {
      if (!element) {
        return doc;
      }

      if (element.nodeType == 9) {
        return element;
      }

      return element.ownerDocument;
    };

    DomQuery.fn = DomQuery.prototype = {
      constructor: DomQuery,

      /**
       * Selector for the current set.
       *
       * @property selector
       * @type String
       */
      selector: "",

      /**
       * Context used to create the set.
       *
       * @property context
       * @type Element
       */
      context: null,

      /**
       * Number of items in the current set.
       *
       * @property length
       * @type Number
       */
      length: 0,

      /**
       * Constructs a new DomQuery instance with the specified selector or context.
       *
       * @constructor
       * @method init
       * @param {String/Array/DomQuery} selector Optional CSS selector/Array or array like object or HTML string.
       * @param {Document/Element} context Optional context to search in.
       */
      init: function (selector, context) {
        var self = this, match, node;

        if (!selector) {
          return self;
        }

        if (selector.nodeType) {
          self.context = self[0] = selector;
          self.length = 1;

          return self;
        }

        if (context && context.nodeType) {
          self.context = context;
        } else {
          if (context) {
            return DomQuery(selector).attr(context);
          }

          self.context = context = document;
        }

        if (isString(selector)) {
          self.selector = selector;

          if (selector.charAt(0) === "<" && selector.charAt(selector.length - 1) === ">" && selector.length >= 3) {
            match = [null, selector, null];
          } else {
            match = rquickExpr.exec(selector);
          }

          if (match) {
            if (match[1]) {
              node = createFragment(selector, getElementDocument(context)).firstChild;

              while (node) {
                push.call(self, node);
                node = node.nextSibling;
              }
            } else {
              node = getElementDocument(context).getElementById(match[2]);

              if (!node) {
                return self;
              }

              if (node.id !== match[2]) {
                return self.find(selector);
              }

              self.length = 1;
              self[0] = node;
            }
          } else {
            return DomQuery(context).find(selector);
          }
        } else {
          this.add(selector, false);
        }

        return self;
      },

      /**
       * Converts the current set to an array.
       *
       * @method toArray
       * @return {Array} Array of all nodes in set.
       */
      toArray: function () {
        return Tools.toArray(this);
      },

      /**
       * Adds new nodes to the set.
       *
       * @method add
       * @param {Array/tinymce.core.dom.DomQuery} items Array of all nodes to add to set.
       * @param {Boolean} sort Optional sort flag that enables sorting of elements.
       * @return {tinymce.dom.DomQuery} New instance with nodes added.
       */
      add: function (items, sort) {
        var self = this, nodes, i;

        if (isString(items)) {
          return self.add(DomQuery(items));
        }

        if (sort !== false) {
          nodes = DomQuery.unique(self.toArray().concat(DomQuery.makeArray(items)));
          self.length = nodes.length;
          for (i = 0; i < nodes.length; i++) {
            self[i] = nodes[i];
          }
        } else {
          push.apply(self, DomQuery.makeArray(items));
        }

        return self;
      },

      /**
       * Sets/gets attributes on the elements in the current set.
       *
       * @method attr
       * @param {String/Object} name Name of attribute to get or an object with attributes to set.
       * @param {String} value Optional value to set.
       * @return {tinymce.dom.DomQuery/String} Current set or the specified attribute when only the name is specified.
       */
      attr: function (name, value) {
        var self = this, hook;

        if (typeof name === "object") {
          each(name, function (name, value) {
            self.attr(name, value);
          });
        } else if (isDefined(value)) {
          this.each(function () {
            var hook;

            if (this.nodeType === 1) {
              hook = attrHooks[name];
              if (hook && hook.set) {
                hook.set(this, value);
                return;
              }

              if (value === null) {
                this.removeAttribute(name, 2);
              } else {
                this.setAttribute(name, value, 2);
              }
            }
          });
        } else {
          if (self[0] && self[0].nodeType === 1) {
            hook = attrHooks[name];
            if (hook && hook.get) {
              return hook.get(self[0], name);
            }

            if (booleanMap[name]) {
              return self.prop(name) ? name : undef;
            }

            value = self[0].getAttribute(name, 2);

            if (value === null) {
              value = undef;
            }
          }

          return value;
        }

        return self;
      },

      /**
       * Removes attributse on the elements in the current set.
       *
       * @method removeAttr
       * @param {String/Object} name Name of attribute to remove.
       * @return {tinymce.dom.DomQuery/String} Current set.
       */
      removeAttr: function (name) {
        return this.attr(name, null);
      },

      /**
       * Sets/gets properties on the elements in the current set.
       *
       * @method attr
       * @param {String/Object} name Name of property to get or an object with properties to set.
       * @param {String} value Optional value to set.
       * @return {tinymce.dom.DomQuery/String} Current set or the specified property when only the name is specified.
       */
      prop: function (name, value) {
        var self = this;

        name = propFix[name] || name;

        if (typeof name === "object") {
          each(name, function (name, value) {
            self.prop(name, value);
          });
        } else if (isDefined(value)) {
          this.each(function () {
            if (this.nodeType == 1) {
              this[name] = value;
            }
          });
        } else {
          if (self[0] && self[0].nodeType && name in self[0]) {
            return self[0][name];
          }

          return value;
        }

        return self;
      },

      /**
       * Sets/gets styles on the elements in the current set.
       *
       * @method css
       * @param {String/Object} name Name of style to get or an object with styles to set.
       * @param {String} value Optional value to set.
       * @return {tinymce.dom.DomQuery/String} Current set or the specified style when only the name is specified.
       */
      css: function (name, value) {
        var self = this, elm, hook;

        var camel = function (name) {
          return name.replace(/-(\D)/g, function (a, b) {
            return b.toUpperCase();
          });
        };

        var dashed = function (name) {
          return name.replace(/[A-Z]/g, function (a) {
            return '-' + a;
          });
        };

        if (typeof name === "object") {
          each(name, function (name, value) {
            self.css(name, value);
          });
        } else {
          if (isDefined(value)) {
            name = camel(name);

            // Default px suffix on these
            if (typeof value === 'number' && !numericCssMap[name]) {
              value += 'px';
            }

            self.each(function () {
              var style = this.style;

              hook = cssHooks[name];
              if (hook && hook.set) {
                hook.set(this, value);
                return;
              }

              try {
                this.style[cssFix[name] || name] = value;
              } catch (ex) {
                // Ignore
              }

              if (value === null || value === '') {
                if (style.removeProperty) {
                  style.removeProperty(dashed(name));
                } else {
                  style.removeAttribute(name);
                }
              }
            });
          } else {
            elm = self[0];

            hook = cssHooks[name];
            if (hook && hook.get) {
              return hook.get(elm);
            }

            if (elm.ownerDocument.defaultView) {
              try {
                return elm.ownerDocument.defaultView.getComputedStyle(elm, null).getPropertyValue(dashed(name));
              } catch (ex) {
                return undef;
              }
            } else if (elm.currentStyle) {
              return elm.currentStyle[camel(name)];
            }
          }
        }

        return self;
      },

      /**
       * Removes all nodes in set from the document.
       *
       * @method remove
       * @return {tinymce.dom.DomQuery} Current set with the removed nodes.
       */
      remove: function () {
        var self = this, node, i = this.length;

        while (i--) {
          node = self[i];
          Event.clean(node);

          if (node.parentNode) {
            node.parentNode.removeChild(node);
          }
        }

        return this;
      },

      /**
       * Empties all elements in set.
       *
       * @method empty
       * @return {tinymce.dom.DomQuery} Current set with the empty nodes.
       */
      empty: function () {
        var self = this, node, i = this.length;

        while (i--) {
          node = self[i];
          while (node.firstChild) {
            node.removeChild(node.firstChild);
          }
        }

        return this;
      },

      /**
       * Sets or gets the HTML of the current set or first set node.
       *
       * @method html
       * @param {String} value Optional innerHTML value to set on each element.
       * @return {tinymce.dom.DomQuery/String} Current set or the innerHTML of the first element.
       */
      html: function (value) {
        var self = this, i;

        if (isDefined(value)) {
          i = self.length;

          try {
            while (i--) {
              self[i].innerHTML = value;
            }
          } catch (ex) {
            // Workaround for "Unknown runtime error" when DIV is added to P on IE
            DomQuery(self[i]).empty().append(value);
          }

          return self;
        }

        return self[0] ? self[0].innerHTML : '';
      },

      /**
       * Sets or gets the text of the current set or first set node.
       *
       * @method text
       * @param {String} value Optional innerText value to set on each element.
       * @return {tinymce.dom.DomQuery/String} Current set or the innerText of the first element.
       */
      text: function (value) {
        var self = this, i;

        if (isDefined(value)) {
          i = self.length;
          while (i--) {
            if ("innerText" in self[i]) {
              self[i].innerText = value;
            } else {
              self[0].textContent = value;
            }
          }

          return self;
        }

        return self[0] ? (self[0].innerText || self[0].textContent) : '';
      },

      /**
       * Appends the specified node/html or node set to the current set nodes.
       *
       * @method append
       * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to append to each element in set.
       * @return {tinymce.dom.DomQuery} Current set.
       */
      append: function () {
        return domManipulate(this, arguments, function (node) {
          // Either element or Shadow Root
          if (this.nodeType === 1 || (this.host && this.host.nodeType === 1)) {
            this.appendChild(node);
          }
        });
      },

      /**
       * Prepends the specified node/html or node set to the current set nodes.
       *
       * @method prepend
       * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to prepend to each element in set.
       * @return {tinymce.dom.DomQuery} Current set.
       */
      prepend: function () {
        return domManipulate(this, arguments, function (node) {
          // Either element or Shadow Root
          if (this.nodeType === 1 || (this.host && this.host.nodeType === 1)) {
            this.insertBefore(node, this.firstChild);
          }
        }, true);
      },

      /**
       * Adds the specified elements before current set nodes.
       *
       * @method before
       * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to add before to each element in set.
       * @return {tinymce.dom.DomQuery} Current set.
       */
      before: function () {
        var self = this;

        if (self[0] && self[0].parentNode) {
          return domManipulate(self, arguments, function (node) {
            this.parentNode.insertBefore(node, this);
          });
        }

        return self;
      },

      /**
       * Adds the specified elements after current set nodes.
       *
       * @method after
       * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to add after to each element in set.
       * @return {tinymce.dom.DomQuery} Current set.
       */
      after: function () {
        var self = this;

        if (self[0] && self[0].parentNode) {
          return domManipulate(self, arguments, function (node) {
            this.parentNode.insertBefore(node, this.nextSibling);
          }, true);
        }

        return self;
      },

      /**
       * Appends the specified set nodes to the specified selector/instance.
       *
       * @method appendTo
       * @param {String/Element/Array/tinymce.dom.DomQuery} val Item to append the current set to.
       * @return {tinymce.dom.DomQuery} Current set with the appended nodes.
       */
      appendTo: function (val) {
        DomQuery(val).append(this);

        return this;
      },

      /**
       * Prepends the specified set nodes to the specified selector/instance.
       *
       * @method prependTo
       * @param {String/Element/Array/tinymce.dom.DomQuery} val Item to prepend the current set to.
       * @return {tinymce.dom.DomQuery} Current set with the prepended nodes.
       */
      prependTo: function (val) {
        DomQuery(val).prepend(this);

        return this;
      },

      /**
       * Replaces the nodes in set with the specified content.
       *
       * @method replaceWith
       * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to replace nodes with.
       * @return {tinymce.dom.DomQuery} Set with replaced nodes.
       */
      replaceWith: function (content) {
        return this.before(content).remove();
      },

      /**
       * Wraps all elements in set with the specified wrapper.
       *
       * @method wrap
       * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to wrap nodes with.
       * @return {tinymce.dom.DomQuery} Set with wrapped nodes.
       */
      wrap: function (content) {
        return wrap(this, content);
      },

      /**
       * Wraps all nodes in set with the specified wrapper. If the nodes are siblings all of them
       * will be wrapped in the same wrapper.
       *
       * @method wrapAll
       * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to wrap nodes with.
       * @return {tinymce.dom.DomQuery} Set with wrapped nodes.
       */
      wrapAll: function (content) {
        return wrap(this, content, true);
      },

      /**
       * Wraps all elements inner contents in set with the specified wrapper.
       *
       * @method wrapInner
       * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to wrap nodes with.
       * @return {tinymce.dom.DomQuery} Set with wrapped nodes.
       */
      wrapInner: function (content) {
        this.each(function () {
          DomQuery(this).contents().wrapAll(content);
        });

        return this;
      },

      /**
       * Unwraps all elements by removing the parent element of each item in set.
       *
       * @method unwrap
       * @return {tinymce.dom.DomQuery} Set with unwrapped nodes.
       */
      unwrap: function () {
        return this.parent().each(function () {
          DomQuery(this).replaceWith(this.childNodes);
        });
      },

      /**
       * Clones all nodes in set.
       *
       * @method clone
       * @return {tinymce.dom.DomQuery} Set with cloned nodes.
       */
      clone: function () {
        var result = [];

        this.each(function () {
          result.push(this.cloneNode(true));
        });

        return DomQuery(result);
      },

      /**
       * Adds the specified class name to the current set elements.
       *
       * @method addClass
       * @param {String} className Class name to add.
       * @return {tinymce.dom.DomQuery} Current set.
       */
      addClass: function (className) {
        return this.toggleClass(className, true);
      },

      /**
       * Removes the specified class name to the current set elements.
       *
       * @method removeClass
       * @param {String} className Class name to remove.
       * @return {tinymce.dom.DomQuery} Current set.
       */
      removeClass: function (className) {
        return this.toggleClass(className, false);
      },

      /**
       * Toggles the specified class name on the current set elements.
       *
       * @method toggleClass
       * @param {String} className Class name to add/remove.
       * @param {Boolean} state Optional state to toggle on/off.
       * @return {tinymce.dom.DomQuery} Current set.
       */
      toggleClass: function (className, state) {
        var self = this;

        // Functions are not supported
        if (typeof className != 'string') {
          return self;
        }

        if (className.indexOf(' ') !== -1) {
          each(className.split(' '), function () {
            self.toggleClass(this, state);
          });
        } else {
          self.each(function (index, node) {
            var existingClassName, classState;

            classState = hasClass(node, className);
            if (classState !== state) {
              existingClassName = node.className;

              if (classState) {
                node.className = trim((" " + existingClassName + " ").replace(' ' + className + ' ', ' '));
              } else {
                node.className += existingClassName ? ' ' + className : className;
              }
            }
          });
        }

        return self;
      },

      /**
       * Returns true/false if the first item in set has the specified class.
       *
       * @method hasClass
       * @param {String} className Class name to check for.
       * @return {Boolean} True/false if the set has the specified class.
       */
      hasClass: function (className) {
        return hasClass(this[0], className);
      },

      /**
       * Executes the callback function for each item DomQuery collection. If you return false in the
       * callback it will break the loop.
       *
       * @method each
       * @param {function} callback Callback function to execute for each item.
       * @return {tinymce.dom.DomQuery} Current set.
       */
      each: function (callback) {
        return each(this, callback);
      },

      /**
       * Binds an event with callback function to the elements in set.
       *
       * @method on
       * @param {String} name Name of the event to bind.
       * @param {function} callback Callback function to execute when the event occurs.
       * @return {tinymce.dom.DomQuery} Current set.
       */
      on: function (name, callback) {
        return this.each(function () {
          Event.bind(this, name, callback);
        });
      },

      /**
       * Unbinds an event with callback function to the elements in set.
       *
       * @method off
       * @param {String} name Optional name of the event to bind.
       * @param {function} callback Optional callback function to execute when the event occurs.
       * @return {tinymce.dom.DomQuery} Current set.
       */
      off: function (name, callback) {
        return this.each(function () {
          Event.unbind(this, name, callback);
        });
      },

      /**
       * Triggers the specified event by name or event object.
       *
       * @method trigger
       * @param {String/Object} name Name of the event to trigger or event object.
       * @return {tinymce.dom.DomQuery} Current set.
       */
      trigger: function (name) {
        return this.each(function () {
          if (typeof name == 'object') {
            Event.fire(this, name.type, name);
          } else {
            Event.fire(this, name);
          }
        });
      },

      /**
       * Shows all elements in set.
       *
       * @method show
       * @return {tinymce.dom.DomQuery} Current set.
       */
      show: function () {
        return this.css('display', '');
      },

      /**
       * Hides all elements in set.
       *
       * @method hide
       * @return {tinymce.dom.DomQuery} Current set.
       */
      hide: function () {
        return this.css('display', 'none');
      },

      /**
       * Slices the current set.
       *
       * @method slice
       * @param {Number} start Start index to slice at.
       * @param {Number} end Optional end index to end slice at.
       * @return {tinymce.dom.DomQuery} Sliced set.
       */
      slice: function () {
        return new DomQuery(slice.apply(this, arguments));
      },

      /**
       * Makes the set equal to the specified index.
       *
       * @method eq
       * @param {Number} index Index to set it equal to.
       * @return {tinymce.dom.DomQuery} Single item set.
       */
      eq: function (index) {
        return index === -1 ? this.slice(index) : this.slice(index, +index + 1);
      },

      /**
       * Makes the set equal to first element in set.
       *
       * @method first
       * @return {tinymce.dom.DomQuery} Single item set.
       */
      first: function () {
        return this.eq(0);
      },

      /**
       * Makes the set equal to last element in set.
       *
       * @method last
       * @return {tinymce.dom.DomQuery} Single item set.
       */
      last: function () {
        return this.eq(-1);
      },

      /**
       * Finds elements by the specified selector for each element in set.
       *
       * @method find
       * @param {String} selector Selector to find elements by.
       * @return {tinymce.dom.DomQuery} Set with matches elements.
       */
      find: function (selector) {
        var i, l, ret = [];

        for (i = 0, l = this.length; i < l; i++) {
          DomQuery.find(selector, this[i], ret);
        }

        return DomQuery(ret);
      },

      /**
       * Filters the current set with the specified selector.
       *
       * @method filter
       * @param {String/function} selector Selector to filter elements by.
       * @return {tinymce.dom.DomQuery} Set with filtered elements.
       */
      filter: function (selector) {
        if (typeof selector == 'function') {
          return DomQuery(grep(this.toArray(), function (item, i) {
            return selector(i, item);
          }));
        }

        return DomQuery(DomQuery.filter(selector, this.toArray()));
      },

      /**
       * Gets the current node or any parent matching the specified selector.
       *
       * @method closest
       * @param {String/Element/tinymce.dom.DomQuery} selector Selector or element to find.
       * @return {tinymce.dom.DomQuery} Set with closest elements.
       */
      closest: function (selector) {
        var result = [];

        if (selector instanceof DomQuery) {
          selector = selector[0];
        }

        this.each(function (i, node) {
          while (node) {
            if (typeof selector == 'string' && DomQuery(node).is(selector)) {
              result.push(node);
              break;
            } else if (node == selector) {
              result.push(node);
              break;
            }

            node = node.parentNode;
          }
        });

        return DomQuery(result);
      },

      /**
       * Returns the offset of the first element in set or sets the top/left css properties of all elements in set.
       *
       * @method offset
       * @param {Object} offset Optional offset object to set on each item.
       * @return {Object/tinymce.dom.DomQuery} Returns the first element offset or the current set if you specified an offset.
       */
      offset: function (offset) {
        var elm, doc, docElm;
        var x = 0, y = 0, pos;

        if (!offset) {
          elm = this[0];

          if (elm) {
            doc = elm.ownerDocument;
            docElm = doc.documentElement;

            if (elm.getBoundingClientRect) {
              pos = elm.getBoundingClientRect();
              x = pos.left + (docElm.scrollLeft || doc.body.scrollLeft) - docElm.clientLeft;
              y = pos.top + (docElm.scrollTop || doc.body.scrollTop) - docElm.clientTop;
            }
          }

          return {
            left: x,
            top: y
          };
        }

        return this.css(offset);
      },

      push: push,
      sort: [].sort,
      splice: [].splice
    };

    // Static members
    Tools.extend(DomQuery, {
      /**
       * Extends the specified object with one or more objects.
       *
       * @static
       * @method extend
       * @param {Object} target Target object to extend with new items.
       * @param {Object..} object Object to extend the target with.
       * @return {Object} Extended input object.
       */
      extend: Tools.extend,

      /**
       * Creates an array out of an array like object.
       *
       * @static
       * @method makeArray
       * @param {Object} object Object to convert to array.
       * @return {Array} Array produced from object.
       */
      makeArray: function (object) {
        if (isWindow(object) || object.nodeType) {
          return [object];
        }

        return Tools.toArray(object);
      },

      /**
       * Returns the index of the specified item inside the array.
       *
       * @static
       * @method inArray
       * @param {Object} item Item to look for.
       * @param {Array} array Array to look for item in.
       * @return {Number} Index of the item or -1.
       */
      inArray: inArray,

      /**
       * Returns true/false if the specified object is an array or not.
       *
       * @static
       * @method isArray
       * @param {Object} array Object to check if it's an array or not.
       * @return {Boolean} True/false if the object is an array.
       */
      isArray: Tools.isArray,

      /**
       * Executes the callback function for each item in array/object. If you return false in the
       * callback it will break the loop.
       *
       * @static
       * @method each
       * @param {Object} obj Object to iterate.
       * @param {function} callback Callback function to execute for each item.
       */
      each: each,

      /**
       * Removes whitespace from the beginning and end of a string.
       *
       * @static
       * @method trim
       * @param {String} str String to remove whitespace from.
       * @return {String} New string with removed whitespace.
       */
      trim: trim,

      /**
       * Filters out items from the input array by calling the specified function for each item.
       * If the function returns false the item will be excluded if it returns true it will be included.
       *
       * @static
       * @method grep
       * @param {Array} array Array of items to loop though.
       * @param {function} callback Function to call for each item. Include/exclude depends on it's return value.
       * @return {Array} New array with values imported and filtered based in input.
       * @example
       * // Filter out some items, this will return an array with 4 and 5
       * var items = DomQuery.grep([1, 2, 3, 4, 5], function(v) {return v > 3;});
       */
      grep: grep,

      // Sizzle
      find: Sizzle,
      expr: Sizzle.selectors,
      unique: Sizzle.uniqueSort,
      text: Sizzle.getText,
      contains: Sizzle.contains,
      filter: function (expr, elems, not) {
        var i = elems.length;

        if (not) {
          expr = ":not(" + expr + ")";
        }

        while (i--) {
          if (elems[i].nodeType != 1) {
            elems.splice(i, 1);
          }
        }

        if (elems.length === 1) {
          elems = DomQuery.find.matchesSelector(elems[0], expr) ? [elems[0]] : [];
        } else {
          elems = DomQuery.find.matches(expr, elems);
        }

        return elems;
      }
    });

    var dir = function (el, prop, until) {
      var matched = [], cur = el[prop];

      if (typeof until != 'string' && until instanceof DomQuery) {
        until = until[0];
      }

      while (cur && cur.nodeType !== 9) {
        if (until !== undefined) {
          if (cur === until) {
            break;
          }

          if (typeof until == 'string' && DomQuery(cur).is(until)) {
            break;
          }
        }

        if (cur.nodeType === 1) {
          matched.push(cur);
        }

        cur = cur[prop];
      }

      return matched;
    };

    var sibling = function (node, siblingName, nodeType, until) {
      var result = [];

      if (until instanceof DomQuery) {
        until = until[0];
      }

      for (; node; node = node[siblingName]) {
        if (nodeType && node.nodeType !== nodeType) {
          continue;
        }

        if (until !== undefined) {
          if (node === until) {
            break;
          }

          if (typeof until == 'string' && DomQuery(node).is(until)) {
            break;
          }
        }

        result.push(node);
      }

      return result;
    };

    var firstSibling = function (node, siblingName, nodeType) {
      for (node = node[siblingName]; node; node = node[siblingName]) {
        if (node.nodeType == nodeType) {
          return node;
        }
      }

      return null;
    };

    each({
      /**
       * Returns a new collection with the parent of each item in current collection matching the optional selector.
       *
       * @method parent
       * @param {Element/tinymce.dom.DomQuery} node Node to match parents against.
       * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching parents.
       */
      parent: function (node) {
        var parent = node.parentNode;

        return parent && parent.nodeType !== 11 ? parent : null;
      },

      /**
       * Returns a new collection with the all the parents of each item in current collection matching the optional selector.
       *
       * @method parents
       * @param {Element/tinymce.dom.DomQuery} node Node to match parents against.
       * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching parents.
       */
      parents: function (node) {
        return dir(node, "parentNode");
      },

      /**
       * Returns a new collection with next sibling of each item in current collection matching the optional selector.
       *
       * @method next
       * @param {Element/tinymce.dom.DomQuery} node Node to match the next element against.
       * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
       */
      next: function (node) {
        return firstSibling(node, 'nextSibling', 1);
      },

      /**
       * Returns a new collection with previous sibling of each item in current collection matching the optional selector.
       *
       * @method prev
       * @param {Element/tinymce.dom.DomQuery} node Node to match the previous element against.
       * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
       */
      prev: function (node) {
        return firstSibling(node, 'previousSibling', 1);
      },

      /**
       * Returns all child elements matching the optional selector.
       *
       * @method children
       * @param {Element/tinymce.dom.DomQuery} node Node to match the elements against.
       * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
       */
      children: function (node) {
        return sibling(node.firstChild, 'nextSibling', 1);
      },

      /**
       * Returns all child nodes matching the optional selector.
       *
       * @method contents
       * @param {Element/tinymce.dom.DomQuery} node Node to get the contents of.
       * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
       */
      contents: function (node) {
        return Tools.toArray((node.nodeName === "iframe" ? node.contentDocument || node.contentWindow.document : node).childNodes);
      }
    }, function (name, fn) {
      DomQuery.fn[name] = function (selector) {
        var self = this, result = [];

        self.each(function () {
          var nodes = fn.call(result, this, selector, result);

          if (nodes) {
            if (DomQuery.isArray(nodes)) {
              result.push.apply(result, nodes);
            } else {
              result.push(nodes);
            }
          }
        });

        // If traversing on multiple elements we might get the same elements twice
        if (this.length > 1) {
          if (!skipUniques[name]) {
            result = DomQuery.unique(result);
          }

          if (name.indexOf('parents') === 0) {
            result = result.reverse();
          }
        }

        result = DomQuery(result);

        if (selector) {
          return result.filter(selector);
        }

        return result;
      };
    });

    each({
      /**
       * Returns a new collection with the all the parents until the matching selector/element
       * of each item in current collection matching the optional selector.
       *
       * @method parentsUntil
       * @param {Element/tinymce.dom.DomQuery} node Node to find parent of.
       * @param {String/Element/tinymce.dom.DomQuery} until Until the matching selector or element.
       * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching parents.
       */
      parentsUntil: function (node, until) {
        return dir(node, "parentNode", until);
      },

      /**
       * Returns a new collection with all next siblings of each item in current collection matching the optional selector.
       *
       * @method nextUntil
       * @param {Element/tinymce.dom.DomQuery} node Node to find next siblings on.
       * @param {String/Element/tinymce.dom.DomQuery} until Until the matching selector or element.
       * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
       */
      nextUntil: function (node, until) {
        return sibling(node, 'nextSibling', 1, until).slice(1);
      },

      /**
       * Returns a new collection with all previous siblings of each item in current collection matching the optional selector.
       *
       * @method prevUntil
       * @param {Element/tinymce.dom.DomQuery} node Node to find previous siblings on.
       * @param {String/Element/tinymce.dom.DomQuery} until Until the matching selector or element.
       * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
       */
      prevUntil: function (node, until) {
        return sibling(node, 'previousSibling', 1, until).slice(1);
      }
    }, function (name, fn) {
      DomQuery.fn[name] = function (selector, filter) {
        var self = this, result = [];

        self.each(function () {
          var nodes = fn.call(result, this, selector, result);

          if (nodes) {
            if (DomQuery.isArray(nodes)) {
              result.push.apply(result, nodes);
            } else {
              result.push(nodes);
            }
          }
        });

        // If traversing on multiple elements we might get the same elements twice
        if (this.length > 1) {
          result = DomQuery.unique(result);

          if (name.indexOf('parents') === 0 || name === 'prevUntil') {
            result = result.reverse();
          }
        }

        result = DomQuery(result);

        if (filter) {
          return result.filter(filter);
        }

        return result;
      };
    });

    /**
     * Returns true/false if the current set items matches the selector.
     *
     * @method is
     * @param {String} selector Selector to match the elements against.
     * @return {Boolean} True/false if the current set matches the selector.
     */
    DomQuery.fn.is = function (selector) {
      return !!selector && this.filter(selector).length > 0;
    };

    DomQuery.fn.init.prototype = DomQuery.fn;

    DomQuery.overrideDefaults = function (callback) {
      var defaults;

      var sub = function (selector, context) {
        defaults = defaults || callback();

        if (arguments.length === 0) {
          selector = defaults.element;
        }

        if (!context) {
          context = defaults.context;
        }

        return new sub.fn.init(selector, context);
      };

      DomQuery.extend(sub, this);

      return sub;
    };

    var appendHooks = function (targetHooks, prop, hooks) {
      each(hooks, function (name, func) {
        targetHooks[name] = targetHooks[name] || {};
        targetHooks[name][prop] = func;
      });
    };

    if (Env.ie && Env.ie < 8) {
      appendHooks(attrHooks, 'get', {
        maxlength: function (elm) {
          var value = elm.maxLength;

          if (value === 0x7fffffff) {
            return undef;
          }

          return value;
        },

        size: function (elm) {
          var value = elm.size;

          if (value === 20) {
            return undef;
          }

          return value;
        },

        'class': function (elm) {
          return elm.className;
        },

        style: function (elm) {
          var value = elm.style.cssText;

          if (value.length === 0) {
            return undef;
          }

          return value;
        }
      });

      appendHooks(attrHooks, 'set', {
        'class': function (elm, value) {
          elm.className = value;
        },

        style: function (elm, value) {
          elm.style.cssText = value;
        }
      });
    }

    if (Env.ie && Env.ie < 9) {
      /*jshint sub:true */
      /*eslint dot-notation: 0*/
      cssFix['float'] = 'styleFloat';

      appendHooks(cssHooks, 'set', {
        opacity: function (elm, value) {
          var style = elm.style;

          if (value === null || value === '') {
            style.removeAttribute('filter');
          } else {
            style.zoom = 1;
            style.filter = 'alpha(opacity=' + (value * 100) + ')';
          }
        }
      });
    }

    DomQuery.attrHooks = attrHooks;
    DomQuery.cssHooks = cssHooks;

    return DomQuery;
  }
);

define(
  'ephox.katamari.api.LazyValue',

  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Option',
    'global!setTimeout'
  ],

  function (Arr, Option, setTimeout) {
    var nu = function (baseFn) {
      var data = Option.none();
      var callbacks = [];

      /** map :: this LazyValue a -> (a -> b) -> LazyValue b */
      var map = function (f) {
        return nu(function (nCallback) {
          get(function (data) {
            nCallback(f(data));
          });
        });
      };

      var get = function (nCallback) {
        if (isReady()) call(nCallback);
        else callbacks.push(nCallback);
      };

      var set = function (x) {
        data = Option.some(x);
        run(callbacks);
        callbacks = [];
      };

      var isReady = function () {
        return data.isSome();
      };

      var run = function (cbs) {
        Arr.each(cbs, call);
      };

      var call = function(cb) {
        data.each(function(x) {
          setTimeout(function() {
            cb(x);
          }, 0);
        });
      };

      // Lazy values cache the value and kick off immediately
      baseFn(set);

      return {
        get: get,
        map: map,
        isReady: isReady
      };
    };

    var pure = function (a) {
      return nu(function (callback) {
        callback(a);
      });
    };

    return {
      nu: nu,
      pure: pure
    };
  }
);
define(
  'ephox.katamari.async.Bounce',

  [
    'global!Array',
    'global!setTimeout'
  ],

  function (Array, setTimeout) {

    var bounce = function(f) {
      return function() {
        var args = Array.prototype.slice.call(arguments);
        var me = this;
        setTimeout(function() {
          f.apply(me, args);
        }, 0);
      };
    };

    return {
      bounce: bounce
    };
  }
);

define(
  'ephox.katamari.api.Future',

  [
    'ephox.katamari.api.LazyValue',
    'ephox.katamari.async.Bounce'
  ],

  /** A future value that is evaluated on demand. The base function is re-evaluated each time 'get' is called. */
  function (LazyValue, Bounce) {
    var nu = function (baseFn) {
      var get = function(callback) {
        baseFn(Bounce.bounce(callback));
      };

      /** map :: this Future a -> (a -> b) -> Future b */
      var map = function (fab) {
        return nu(function (callback) {
          get(function (a) {
            var value = fab(a);
            callback(value);
          });
        });
      };

      /** bind :: this Future a -> (a -> Future b) -> Future b */
      var bind = function (aFutureB) {
        return nu(function (callback) {
          get(function (a) {
            aFutureB(a).get(callback);
          });
        });
      };

      /** anonBind :: this Future a -> Future b -> Future b
       *  Returns a future, which evaluates the first future, ignores the result, then evaluates the second.
       */
      var anonBind = function (futureB) {
        return nu(function (callback) {
          get(function (a) {
            futureB.get(callback);
          });
        });
      };

      var toLazy = function () {
        return LazyValue.nu(get);
      };

      return {
        map: map,
        bind: bind,
        anonBind: anonBind,
        toLazy: toLazy,
        get: get
      };

    };

    /** a -> Future a */
    var pure = function (a) {
      return nu(function (callback) {
        callback(a);
      });
    };

    return {
      nu: nu,
      pure: pure
    };
  }
);

define(
  'ephox.katamari.async.AsyncValues',

  [
    'ephox.katamari.api.Arr'
  ],

  function (Arr) {
    /* 
     * NOTE: an `asyncValue` must have a `get` function which gets given a callback and calls 
     * that callback with a value once it is ready
     *
     * e.g 
     * {
     *   get: function (callback) { callback(10); }
     * }
     */
    var par = function (asyncValues, nu) {
      return nu(function(callback) {
        var r = [];
        var count = 0;

        var cb = function(i) {
          return function(value) {
            r[i] = value;
            count++;
            if (count >= asyncValues.length) {
              callback(r);
            }
          };
        };

        if (asyncValues.length === 0) {
          callback([]);
        } else {
          Arr.each(asyncValues, function(asyncValue, i) {
            asyncValue.get(cb(i));
          });
        }
      });
    };

    return {
      par: par
    };
  }
);
define(
  'ephox.katamari.api.Futures',

  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Future',
    'ephox.katamari.async.AsyncValues'
  ],

  function (Arr, Future, AsyncValues) {
    /** par :: [Future a] -> Future [a] */
    var par = function(futures) {
      return AsyncValues.par(futures, Future.nu);
    };

    /** mapM :: [a] -> (a -> Future b) -> Future [b] */
    var mapM = function(array, fn) {
      var futures = Arr.map(array, fn);
      return par(futures);
    };

    /** Kleisli composition of two functions: a -> Future b.
     *  Note the order of arguments: g is invoked first, then the result passed to f.
     *  This is in line with f . g = \x -> f (g a)
     *
     *  compose :: ((b -> Future c), (a -> Future b)) -> a -> Future c
     */
    var compose = function (f, g) {
      return function (a) {
        return g(a).bind(f);
      };
    };

    return {
      par: par,
      mapM: mapM,
      compose: compose
    };
  }
);
define(
  'ephox.katamari.api.Result',

  [
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option'
  ],

  function (Fun, Option) {
    /* The type signatures for Result 
     * is :: this Result a -> a -> Bool
     * or :: this Result a -> Result a -> Result a
     * orThunk :: this Result a -> (_ -> Result a) -> Result a
     * map :: this Result a -> (a -> b) -> Result b
     * each :: this Result a -> (a -> _) -> _ 
     * bind :: this Result a -> (a -> Result b) -> Result b
     * fold :: this Result a -> (_ -> b, a -> b) -> b
     * exists :: this Result a -> (a -> Bool) -> Bool
     * forall :: this Result a -> (a -> Bool) -> Bool
     * toOption :: this Result a -> Option a
     * isValue :: this Result a -> Bool
     * isError :: this Result a -> Bool
     * getOr :: this Result a -> a -> a
     * getOrThunk :: this Result a -> (_ -> a) -> a
     * getOrDie :: this Result a -> a (or throws error)
    */

    var value = function (o) {
      var is = function (v) {
        return o === v;      
      };

      var or = function (opt) {
        return value(o);
      };

      var orThunk = function (f) {
        return value(o);
      };

      var map = function (f) {
        return value(f(o));
      };

      var each = function (f) {
        f(o);
      };

      var bind = function (f) {
        return f(o);
      };

      var fold = function (_, onValue) {
        return onValue(o);
      };

      var exists = function (f) {
        return f(o);
      };

      var forall = function (f) {
        return f(o);
      };

      var toOption = function () {
        return Option.some(o);
      };
     
      return {
        is: is,
        isValue: Fun.constant(true),
        isError: Fun.constant(false),
        getOr: Fun.constant(o),
        getOrThunk: Fun.constant(o),
        getOrDie: Fun.constant(o),
        or: or,
        orThunk: orThunk,
        fold: fold,
        map: map,
        each: each,
        bind: bind,
        exists: exists,
        forall: forall,
        toOption: toOption
      };
    };

    var error = function (message) {
      var getOrThunk = function (f) {
        return f();
      };

      var getOrDie = function () {
        return Fun.die(message)();
      };

      var or = function (opt) {
        return opt;
      };

      var orThunk = function (f) {
        return f();
      };

      var map = function (f) {
        return error(message);
      };

      var bind = function (f) {
        return error(message);
      };

      var fold = function (onError, _) {
        return onError(message);
      };

      return {
        is: Fun.constant(false),
        isValue: Fun.constant(false),
        isError: Fun.constant(true),
        getOr: Fun.identity,
        getOrThunk: getOrThunk,
        getOrDie: getOrDie,
        or: or,
        orThunk: orThunk,
        fold: fold,
        map: map,
        each: Fun.noop,
        bind: bind,
        exists: Fun.constant(false),
        forall: Fun.constant(true),
        toOption: Option.none
      };
    };

    return {
      value: value,
      error: error
    };
  }
);

/**
 * StyleSheetLoader.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
 */

/**
 * This class handles loading of external stylesheets and fires events when these are loaded.
 *
 * @class tinymce.dom.StyleSheetLoader
 * @private
 */
define(
  'tinymce.core.dom.StyleSheetLoader',
  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Future',
    'ephox.katamari.api.Futures',
    'ephox.katamari.api.Result',
    'global!navigator',
    'tinymce.core.util.Delay',
    'tinymce.core.util.Tools'
  ],
  function (Arr, Fun, Future, Futures, Result, navigator, Delay, Tools) {
    "use strict";

    return function (document, settings) {
      var idCount = 0, loadedStates = {}, maxLoadTime;

      settings = settings || {};
      maxLoadTime = settings.maxLoadTime || 5000;

      var appendToHead = function (node) {
        document.getElementsByTagName('head')[0].appendChild(node);
      };

      /**
       * Loads the specified css style sheet file and call the loadedCallback once it's finished loading.
       *
       * @method load
       * @param {String} url Url to be loaded.
       * @param {Function} loadedCallback Callback to be executed when loaded.
       * @param {Function} errorCallback Callback to be executed when failed loading.
       */
      var load = function (url, loadedCallback, errorCallback) {
        var link, style, startTime, state;

        var passed = function () {
          var callbacks = state.passed, i = callbacks.length;

          while (i--) {
            callbacks[i]();
          }

          state.status = 2;
          state.passed = [];
          state.failed = [];
        };

        var failed = function () {
          var callbacks = state.failed, i = callbacks.length;

          while (i--) {
            callbacks[i]();
          }

          state.status = 3;
          state.passed = [];
          state.failed = [];
        };

        // Sniffs for older WebKit versions that have the link.onload but a broken one
        var isOldWebKit = function () {
          var webKitChunks = navigator.userAgent.match(/WebKit\/(\d*)/);
          return !!(webKitChunks && webKitChunks[1] < 536);
        };

        // Calls the waitCallback until the test returns true or the timeout occurs
        var wait = function (testCallback, waitCallback) {
          if (!testCallback()) {
            // Wait for timeout
            if ((new Date().getTime()) - startTime < maxLoadTime) {
              Delay.setTimeout(waitCallback);
            } else {
              failed();
            }
          }
        };

        // Workaround for WebKit that doesn't properly support the onload event for link elements
        // Or WebKit that fires the onload event before the StyleSheet is added to the document
        var waitForWebKitLinkLoaded = function () {
          wait(function () {
            var styleSheets = document.styleSheets, styleSheet, i = styleSheets.length, owner;

            while (i--) {
              styleSheet = styleSheets[i];
              owner = styleSheet.ownerNode ? styleSheet.ownerNode : styleSheet.owningElement;
              if (owner && owner.id === link.id) {
                passed();
                return true;
              }
            }
          }, waitForWebKitLinkLoaded);
        };

        // Workaround for older Geckos that doesn't have any onload event for StyleSheets
        var waitForGeckoLinkLoaded = function () {
          wait(function () {
            try {
              // Accessing the cssRules will throw an exception until the CSS file is loaded
              var cssRules = style.sheet.cssRules;
              passed();
              return !!cssRules;
            } catch (ex) {
              // Ignore
            }
          }, waitForGeckoLinkLoaded);
        };

        url = Tools._addCacheSuffix(url);

        if (!loadedStates[url]) {
          state = {
            passed: [],
            failed: []
          };

          loadedStates[url] = state;
        } else {
          state = loadedStates[url];
        }

        if (loadedCallback) {
          state.passed.push(loadedCallback);
        }

        if (errorCallback) {
          state.failed.push(errorCallback);
        }

        // Is loading wait for it to pass
        if (state.status == 1) {
          return;
        }

        // Has finished loading and was success
        if (state.status == 2) {
          passed();
          return;
        }

        // Has finished loading and was a failure
        if (state.status == 3) {
          failed();
          return;
        }

        // Start loading
        state.status = 1;
        link = document.createElement('link');
        link.rel = 'stylesheet';
        link.type = 'text/css';
        link.id = 'u' + (idCount++);
        link.async = false;
        link.defer = false;
        startTime = new Date().getTime();

        // Feature detect onload on link element and sniff older webkits since it has an broken onload event
        if ("onload" in link && !isOldWebKit()) {
          link.onload = waitForWebKitLinkLoaded;
          link.onerror = failed;
        } else {
          // Sniff for old Firefox that doesn't support the onload event on link elements
          // TODO: Remove this in the future when everyone uses modern browsers
          if (navigator.userAgent.indexOf("Firefox") > 0) {
            style = document.createElement('style');
            style.textContent = '@import "' + url + '"';
            waitForGeckoLinkLoaded();
            appendToHead(style);
            return;
          }

          // Use the id owner on older webkits
          waitForWebKitLinkLoaded();
        }

        appendToHead(link);
        link.href = url;
      };

      var loadF = function (url) {
        return Future.nu(function (resolve) {
          load(
            url,
            Fun.compose(resolve, Fun.constant(Result.value(url))),
            Fun.compose(resolve, Fun.constant(Result.error(url)))
          );
        });
      };

      var unbox = function (result) {
        return result.fold(Fun.identity, Fun.identity);
      };

      var loadAll = function (urls, success, failure) {
        Futures.par(Arr.map(urls, loadF)).get(function (result) {
          var parts = Arr.partition(result, function (r) {
            return r.isValue();
          });

          if (parts.fail.length > 0) {
            failure(parts.fail.map(unbox));
          } else {
            success(parts.pass.map(unbox));
          }
        });
      };

      return {
        load: load,
        loadAll: loadAll
      };
    };
  }
);

/**
 * TreeWalker.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
 */

/**
 * TreeWalker class enables you to walk the DOM in a linear manner.
 *
 * @class tinymce.dom.TreeWalker
 * @example
 * var walker = new tinymce.dom.TreeWalker(startNode);
 *
 * do {
 *     console.log(walker.current());
 * } while (walker.next());
 */
define(
  'tinymce.core.dom.TreeWalker',
  [
  ],
  function () {
    /**
     * Constructs a new TreeWalker instance.
     *
     * @constructor
     * @method TreeWalker
     * @param {Node} startNode Node to start walking from.
     * @param {node} rootNode Optional root node to never walk out of.
     */
    return function (startNode, rootNode) {
      var node = startNode;

      var findSibling = function (node, startName, siblingName, shallow) {
        var sibling, parent;

        if (node) {
          // Walk into nodes if it has a start
          if (!shallow && node[startName]) {
            return node[startName];
          }

          // Return the sibling if it has one
          if (node != rootNode) {
            sibling = node[siblingName];
            if (sibling) {
              return sibling;
            }

            // Walk up the parents to look for siblings
            for (parent = node.parentNode; parent && parent != rootNode; parent = parent.parentNode) {
              sibling = parent[siblingName];
              if (sibling) {
                return sibling;
              }
            }
          }
        }
      };

      var findPreviousNode = function (node, startName, siblingName, shallow) {
        var sibling, parent, child;

        if (node) {
          sibling = node[siblingName];
          if (rootNode && sibling === rootNode) {
            return;
          }

          if (sibling) {
            if (!shallow) {
              // Walk up the parents to look for siblings
              for (child = sibling[startName]; child; child = child[startName]) {
                if (!child[startName]) {
                  return child;
                }
              }
            }

            return sibling;
          }

          parent = node.parentNode;
          if (parent && parent !== rootNode) {
            return parent;
          }
        }
      };

      /**
       * Returns the current node.
       *
       * @method current
       * @return {Node} Current node where the walker is.
       */
      this.current = function () {
        return node;
      };

      /**
       * Walks to the next node in tree.
       *
       * @method next
       * @return {Node} Current node where the walker is after moving to the next node.
       */
      this.next = function (shallow) {
        node = findSibling(node, 'firstChild', 'nextSibling', shallow);
        return node;
      };

      /**
       * Walks to the previous node in tree.
       *
       * @method prev
       * @return {Node} Current node where the walker is after moving to the previous node.
       */
      this.prev = function (shallow) {
        node = findSibling(node, 'lastChild', 'previousSibling', shallow);
        return node;
      };

      this.prev2 = function (shallow) {
        node = findPreviousNode(node, 'lastChild', 'previousSibling', shallow);
        return node;
      };
    };
  }
);

define("global!console", [], function () { if (typeof console === "undefined") console = { log: function () {} }; return console; });
define(
  'ephox.sugar.api.node.Element',

  [
    'ephox.katamari.api.Fun',
    'global!Error',
    'global!console',
    'global!document'
  ],

  function (Fun, Error, console, document) {
    var fromHtml = function (html, scope) {
      var doc = scope || document;
      var div = doc.createElement('div');
      div.innerHTML = html;
      if (!div.hasChildNodes() || div.childNodes.length > 1) {
        console.error('HTML does not have a single root node', html);
        throw 'HTML must have a single root node';
      }
      return fromDom(div.childNodes[0]);
    };

    var fromTag = function (tag, scope) {
      var doc = scope || document;
      var node = doc.createElement(tag);
      return fromDom(node);
    };

    var fromText = function (text, scope) {
      var doc = scope || document;
      var node = doc.createTextNode(text);
      return fromDom(node);
    };

    var fromDom = function (node) {
      if (node === null || node === undefined) throw new Error('Node cannot be null or undefined');
      return {
        dom: Fun.constant(node)
      };
    };

    return {
      fromHtml: fromHtml,
      fromTag: fromTag,
      fromText: fromText,
      fromDom: fromDom
    };
  }
);

/**
 * Entities.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
 */

/*jshint bitwise:false */
/*eslint no-bitwise:0 */

/**
 * Entity encoder class.
 *
 * @class tinymce.html.Entities
 * @static
 * @version 3.4
 */
define(
  'tinymce.core.html.Entities',
  [
    'ephox.sugar.api.node.Element',
    'tinymce.core.util.Tools'
  ],
  function (Element, Tools) {
    var makeMap = Tools.makeMap;

    var namedEntities, baseEntities, reverseEntities,
      attrsCharsRegExp = /[&<>\"\u0060\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
      textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
      rawCharsRegExp = /[<>&\"\']/g,
      entityRegExp = /&#([a-z0-9]+);?|&([a-z0-9]+);/gi,
      asciiMap = {
        128: "\u20AC", 130: "\u201A", 131: "\u0192", 132: "\u201E", 133: "\u2026", 134: "\u2020",
        135: "\u2021", 136: "\u02C6", 137: "\u2030", 138: "\u0160", 139: "\u2039", 140: "\u0152",
        142: "\u017D", 145: "\u2018", 146: "\u2019", 147: "\u201C", 148: "\u201D", 149: "\u2022",
        150: "\u2013", 151: "\u2014", 152: "\u02DC", 153: "\u2122", 154: "\u0161", 155: "\u203A",
        156: "\u0153", 158: "\u017E", 159: "\u0178"
      };

    // Raw entities
    baseEntities = {
      '\"': '&quot;', // Needs to be escaped since the YUI compressor would otherwise break the code
      "'": '&#39;',
      '<': '&lt;',
      '>': '&gt;',
      '&': '&amp;',
      '\u0060': '&#96;'
    };

    // Reverse lookup table for raw entities
    reverseEntities = {
      '&lt;': '<',
      '&gt;': '>',
      '&amp;': '&',
      '&quot;': '"',
      '&apos;': "'"
    };

    // Decodes text by using the browser
    var nativeDecode = function (text) {
      var elm;

      elm = Element.fromTag("div").dom();
      elm.innerHTML = text;

      return elm.textContent || elm.innerText || text;
    };

    // Build a two way lookup table for the entities
    var buildEntitiesLookup = function (items, radix) {
      var i, chr, entity, lookup = {};

      if (items) {
        items = items.split(',');
        radix = radix || 10;

        // Build entities lookup table
        for (i = 0; i < items.length; i += 2) {
          chr = String.fromCharCode(parseInt(items[i], radix));

          // Only add non base entities
          if (!baseEntities[chr]) {
            entity = '&' + items[i + 1] + ';';
            lookup[chr] = entity;
            lookup[entity] = chr;
          }
        }

        return lookup;
      }
    };

    // Unpack entities lookup where the numbers are in radix 32 to reduce the size
    namedEntities = buildEntitiesLookup(
      '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' +
      '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' +
      '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' +
      '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' +
      '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' +
      '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' +
      '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' +
      '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' +
      '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' +
      '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' +
      'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' +
      'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' +
      't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' +
      'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' +
      'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' +
      '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' +
      '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' +
      '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' +
      '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' +
      '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' +
      'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' +
      'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' +
      'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' +
      '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' +
      '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro', 32);

    var Entities = {
      /**
       * Encodes the specified string using raw entities. This means only the required XML base entities will be encoded.
       *
       * @method encodeRaw
       * @param {String} text Text to encode.
       * @param {Boolean} attr Optional flag to specify if the text is attribute contents.
       * @return {String} Entity encoded text.
       */
      encodeRaw: function (text, attr) {
        return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function (chr) {
          return baseEntities[chr] || chr;
        });
      },

      /**
       * Encoded the specified text with both the attributes and text entities. This function will produce larger text contents
       * since it doesn't know if the context is within a attribute or text node. This was added for compatibility
       * and is exposed as the DOMUtils.encode function.
       *
       * @method encodeAllRaw
       * @param {String} text Text to encode.
       * @return {String} Entity encoded text.
       */
      encodeAllRaw: function (text) {
        return ('' + text).replace(rawCharsRegExp, function (chr) {
          return baseEntities[chr] || chr;
        });
      },

      /**
       * Encodes the specified string using numeric entities. The core entities will be
       * encoded as named ones but all non lower ascii characters will be encoded into numeric entities.
       *
       * @method encodeNumeric
       * @param {String} text Text to encode.
       * @param {Boolean} attr Optional flag to specify if the text is attribute contents.
       * @return {String} Entity encoded text.
       */
      encodeNumeric: function (text, attr) {
        return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function (chr) {
          // Multi byte sequence convert it to a single entity
          if (chr.length > 1) {
            return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';';
          }

          return baseEntities[chr] || '&#' + chr.charCodeAt(0) + ';';
        });
      },

      /**
       * Encodes the specified string using named entities. The core entities will be encoded
       * as named ones but all non lower ascii characters will be encoded into named entities.
       *
       * @method encodeNamed
       * @param {String} text Text to encode.
       * @param {Boolean} attr Optional flag to specify if the text is attribute contents.
       * @param {Object} entities Optional parameter with entities to use.
       * @return {String} Entity encoded text.
       */
      encodeNamed: function (text, attr, entities) {
        entities = entities || namedEntities;

        return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function (chr) {
          return baseEntities[chr] || entities[chr] || chr;
        });
      },

      /**
       * Returns an encode function based on the name(s) and it's optional entities.
       *
       * @method getEncodeFunc
       * @param {String} name Comma separated list of encoders for example named,numeric.
       * @param {String} entities Optional parameter with entities to use instead of the built in set.
       * @return {function} Encode function to be used.
       */
      getEncodeFunc: function (name, entities) {
        entities = buildEntitiesLookup(entities) || namedEntities;

        var encodeNamedAndNumeric = function (text, attr) {
          return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function (chr) {
            if (baseEntities[chr] !== undefined) {
              return baseEntities[chr];
            }

            if (entities[chr] !== undefined) {
              return entities[chr];
            }

            // Convert multi-byte sequences to a single entity.
            if (chr.length > 1) {
              return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';';
            }

            return '&#' + chr.charCodeAt(0) + ';';
          });
        };

        var encodeCustomNamed = function (text, attr) {
          return Entities.encodeNamed(text, attr, entities);
        };

        // Replace + with , to be compatible with previous TinyMCE versions
        name = makeMap(name.replace(/\+/g, ','));

        // Named and numeric encoder
        if (name.named && name.numeric) {
          return encodeNamedAndNumeric;
        }

        // Named encoder
        if (name.named) {
          // Custom names
          if (entities) {
            return encodeCustomNamed;
          }

          return Entities.encodeNamed;
        }

        // Numeric
        if (name.numeric) {
          return Entities.encodeNumeric;
        }

        // Raw encoder
        return Entities.encodeRaw;
      },

      /**
       * Decodes the specified string, this will replace entities with raw UTF characters.
       *
       * @method decode
       * @param {String} text Text to entity decode.
       * @return {String} Entity decoded string.
       */
      decode: function (text) {
        return text.replace(entityRegExp, function (all, numeric) {
          if (numeric) {
            if (numeric.charAt(0).toLowerCase() === 'x') {
              numeric = parseInt(numeric.substr(1), 16);
            } else {
              numeric = parseInt(numeric, 10);
            }

            // Support upper UTF
            if (numeric > 0xFFFF) {
              numeric -= 0x10000;

              return String.fromCharCode(0xD800 + (numeric >> 10), 0xDC00 + (numeric & 0x3FF));
            }

            return asciiMap[numeric] || String.fromCharCode(numeric);
          }

          return reverseEntities[all] || namedEntities[all] || nativeDecode(all);
        });
      }
    };

    return Entities;
  }
);

/**
 * Schema.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
 */

/**
 * Schema validator class.
 *
 * @class tinymce.html.Schema
 * @example
 *  if (tinymce.activeEditor.schema.isValidChild('p', 'span'))
 *    alert('span is valid child of p.');
 *
 *  if (tinymce.activeEditor.schema.getElementRule('p'))
 *    alert('P is a valid element.');
 *
 * @class tinymce.html.Schema
 * @version 3.4
 */
define(
  'tinymce.core.html.Schema',
  [
    "tinymce.core.util.Tools"
  ],
  function (Tools) {
    var mapCache = {}, dummyObj = {};
    var makeMap = Tools.makeMap, each = Tools.each, extend = Tools.extend, explode = Tools.explode, inArray = Tools.inArray;

    var split = function (items, delim) {
      items = Tools.trim(items);
      return items ? items.split(delim || ' ') : [];
    };

    /**
     * Builds a schema lookup table
     *
     * @private
     * @param {String} type html4, html5 or html5-strict schema type.
     * @return {Object} Schema lookup table.
     */
    var compileSchema = function (type) {
      var schema = {}, globalAttributes, blockContent;
      var phrasingContent, flowContent, html4BlockContent, html4PhrasingContent;

      var add = function (name, attributes, children) {
        var ni, attributesOrder, element;

        var arrayToMap = function (array, obj) {
          var map = {}, i, l;

          for (i = 0, l = array.length; i < l; i++) {
            map[array[i]] = obj || {};
          }

          return map;
        };

        children = children || [];
        attributes = attributes || "";

        if (typeof children === "string") {
          children = split(children);
        }

        name = split(name);
        ni = name.length;
        while (ni--) {
          attributesOrder = split([globalAttributes, attributes].join(' '));

          element = {
            attributes: arrayToMap(attributesOrder),
            attributesOrder: attributesOrder,
            children: arrayToMap(children, dummyObj)
          };

          schema[name[ni]] = element;
        }
      };

      var addAttrs = function (name, attributes) {
        var ni, schemaItem, i, l;

        name = split(name);
        ni = name.length;
        attributes = split(attributes);
        while (ni--) {
          schemaItem = schema[name[ni]];
          for (i = 0, l = attributes.length; i < l; i++) {
            schemaItem.attributes[attributes[i]] = {};
            schemaItem.attributesOrder.push(attributes[i]);
          }
        }
      };

      // Use cached schema
      if (mapCache[type]) {
        return mapCache[type];
      }

      // Attributes present on all elements
      globalAttributes = "id accesskey class dir lang style tabindex title role";

      // Event attributes can be opt-in/opt-out
      /*eventAttributes = split("onabort onblur oncancel oncanplay oncanplaythrough onchange onclick onclose oncontextmenu oncuechange " +
       "ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended " +
       "onerror onfocus oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart " +
       "onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onpause onplay onplaying onprogress onratechange " +
       "onreset onscroll onseeked onseeking onseeking onselect onshow onstalled onsubmit onsuspend ontimeupdate onvolumechange " +
       "onwaiting"
       );*/

      // Block content elements
      blockContent =
        "address blockquote div dl fieldset form h1 h2 h3 h4 h5 h6 hr menu ol p pre table ul";

      // Phrasing content elements from the HTML5 spec (inline)
      phrasingContent =
        "a abbr b bdo br button cite code del dfn em embed i iframe img input ins kbd " +
        "label map noscript object q s samp script select small span strong sub sup " +
        "textarea u var #text #comment"
        ;

      // Add HTML5 items to globalAttributes, blockContent, phrasingContent
      if (type != "html4") {
        globalAttributes += " contenteditable contextmenu draggable dropzone " +
          "hidden spellcheck translate";
        blockContent += " article aside details dialog figure header footer hgroup section nav";
        phrasingContent += " audio canvas command datalist mark meter output picture " +
          "progress time wbr video ruby bdi keygen";
      }

      // Add HTML4 elements unless it's html5-strict
      if (type != "html5-strict") {
        globalAttributes += " xml:lang";

        html4PhrasingContent = "acronym applet basefont big font strike tt";
        phrasingContent = [phrasingContent, html4PhrasingContent].join(' ');

        each(split(html4PhrasingContent), function (name) {
          add(name, "", phrasingContent);
        });

        html4BlockContent = "center dir isindex noframes";
        blockContent = [blockContent, html4BlockContent].join(' ');

        // Flow content elements from the HTML5 spec (block+inline)
        flowContent = [blockContent, phrasingContent].join(' ');

        each(split(html4BlockContent), function (name) {
          add(name, "", flowContent);
        });
      }

      // Flow content elements from the HTML5 spec (block+inline)
      flowContent = flowContent || [blockContent, phrasingContent].join(" ");

      // HTML4 base schema TODO: Move HTML5 specific attributes to HTML5 specific if statement
      // Schema items <element name>, <specific attributes>, <children ..>
      add("html", "manifest", "head body");
      add("head", "", "base command link meta noscript script style title");
      add("title hr noscript br");
      add("base", "href target");
      add("link", "href rel media hreflang type sizes hreflang");
      add("meta", "name http-equiv content charset");
      add("style", "media type scoped");
      add("script", "src async defer type charset");
      add("body", "onafterprint onbeforeprint onbeforeunload onblur onerror onfocus " +
        "onhashchange onload onmessage onoffline ononline onpagehide onpageshow " +
        "onpopstate onresize onscroll onstorage onunload", flowContent);
      add("address dt dd div caption", "", flowContent);
      add("h1 h2 h3 h4 h5 h6 pre p abbr code var samp kbd sub sup i b u bdo span legend em strong small s cite dfn", "", phrasingContent);
      add("blockquote", "cite", flowContent);
      add("ol", "reversed start type", "li");
      add("ul", "", "li");
      add("li", "value", flowContent);
      add("dl", "", "dt dd");
      add("a", "href target rel media hreflang type", phrasingContent);
      add("q", "cite", phrasingContent);
      add("ins del", "cite datetime", flowContent);
      add("img", "src sizes srcset alt usemap ismap width height");
      add("iframe", "src name width height", flowContent);
      add("embed", "src type width height");
      add("object", "data type typemustmatch name usemap form width height", [flowContent, "param"].join(' '));
      add("param", "name value");
      add("map", "name", [flowContent, "area"].join(' '));
      add("area", "alt coords shape href target rel media hreflang type");
      add("table", "border", "caption colgroup thead tfoot tbody tr" + (type == "html4" ? " col" : ""));
      add("colgroup", "span", "col");
      add("col", "span");
      add("tbody thead tfoot", "", "tr");
      add("tr", "", "td th");
      add("td", "colspan rowspan headers", flowContent);
      add("th", "colspan rowspan headers scope abbr", flowContent);
      add("form", "accept-charset action autocomplete enctype method name novalidate target", flowContent);
      add("fieldset", "disabled form name", [flowContent, "legend"].join(' '));
      add("label", "form for", phrasingContent);
      add("input", "accept alt autocomplete checked dirname disabled form formaction formenctype formmethod formnovalidate " +
        "formtarget height list max maxlength min multiple name pattern readonly required size src step type value width"
      );
      add("button", "disabled form formaction formenctype formmethod formnovalidate formtarget name type value",
        type == "html4" ? flowContent : phrasingContent);
      add("select", "disabled form multiple name required size", "option optgroup");
      add("optgroup", "disabled label", "option");
      add("option", "disabled label selected value");
      add("textarea", "cols dirname disabled form maxlength name readonly required rows wrap");
      add("menu", "type label", [flowContent, "li"].join(' '));
      add("noscript", "", flowContent);

      // Extend with HTML5 elements
      if (type != "html4") {
        add("wbr");
        add("ruby", "", [phrasingContent, "rt rp"].join(' '));
        add("figcaption", "", flowContent);
        add("mark rt rp summary bdi", "", phrasingContent);
        add("canvas", "width height", flowContent);
        add("video", "src crossorigin poster preload autoplay mediagroup loop " +
          "muted controls width height buffered", [flowContent, "track source"].join(' '));
        add("audio", "src crossorigin preload autoplay mediagroup loop muted controls " +
          "buffered volume", [flowContent, "track source"].join(' '));
        add("picture", "", "img source");
        add("source", "src srcset type media sizes");
        add("track", "kind src srclang label default");
        add("datalist", "", [phrasingContent, "option"].join(' '));
        add("article section nav aside header footer", "", flowContent);
        add("hgroup", "", "h1 h2 h3 h4 h5 h6");
        add("figure", "", [flowContent, "figcaption"].join(' '));
        add("time", "datetime", phrasingContent);
        add("dialog", "open", flowContent);
        add("command", "type label icon disabled checked radiogroup command");
        add("output", "for form name", phrasingContent);
        add("progress", "value max", phrasingContent);
        add("meter", "value min max low high optimum", phrasingContent);
        add("details", "open", [flowContent, "summary"].join(' '));
        add("keygen", "autofocus challenge disabled form keytype name");
      }

      // Extend with HTML4 attributes unless it's html5-strict
      if (type != "html5-strict") {
        addAttrs("script", "language xml:space");
        addAttrs("style", "xml:space");
        addAttrs("object", "declare classid code codebase codetype archive standby align border hspace vspace");
        addAttrs("embed", "align name hspace vspace");
        addAttrs("param", "valuetype type");
        addAttrs("a", "charset name rev shape coords");
        addAttrs("br", "clear");
        addAttrs("applet", "codebase archive code object alt name width height align hspace vspace");
        addAttrs("img", "name longdesc align border hspace vspace");
        addAttrs("iframe", "longdesc frameborder marginwidth marginheight scrolling align");
        addAttrs("font basefont", "size color face");
        addAttrs("input", "usemap align");
        addAttrs("select", "onchange");
        addAttrs("textarea");
        addAttrs("h1 h2 h3 h4 h5 h6 div p legend caption", "align");
        addAttrs("ul", "type compact");
        addAttrs("li", "type");
        addAttrs("ol dl menu dir", "compact");
        addAttrs("pre", "width xml:space");
        addAttrs("hr", "align noshade size width");
        addAttrs("isindex", "prompt");
        addAttrs("table", "summary width frame rules cellspacing cellpadding align bgcolor");
        addAttrs("col", "width align char charoff valign");
        addAttrs("colgroup", "width align char charoff valign");
        addAttrs("thead", "align char charoff valign");
        addAttrs("tr", "align char charoff valign bgcolor");
        addAttrs("th", "axis align char charoff valign nowrap bgcolor width height");
        addAttrs("form", "accept");
        addAttrs("td", "abbr axis scope align char charoff valign nowrap bgcolor width height");
        addAttrs("tfoot", "align char charoff valign");
        addAttrs("tbody", "align char charoff valign");
        addAttrs("area", "nohref");
        addAttrs("body", "background bgcolor text link vlink alink");
      }

      // Extend with HTML5 attributes unless it's html4
      if (type != "html4") {
        addAttrs("input button select textarea", "autofocus");
        addAttrs("input textarea", "placeholder");
        addAttrs("a", "download");
        addAttrs("link script img", "crossorigin");
        addAttrs("iframe", "sandbox seamless allowfullscreen"); // Excluded: srcdoc
      }

      // Special: iframe, ruby, video, audio, label

      // Delete children of the same name from it's parent
      // For example: form can't have a child of the name form
      each(split('a form meter progress dfn'), function (name) {
        if (schema[name]) {
          delete schema[name].children[name];
        }
      });

      // Delete header, footer, sectioning and heading content descendants
      /*each('dt th address', function(name) {
       delete schema[name].children[name];
       });*/

      // Caption can't have tables
      delete schema.caption.children.table;

      // Delete scripts by default due to possible XSS
      delete schema.script;

      // TODO: LI:s can only have value if parent is OL

      // TODO: Handle transparent elements
      // a ins del canvas map

      mapCache[type] = schema;

      return schema;
    };

    var compileElementMap = function (value, mode) {
      var styles;

      if (value) {
        styles = {};

        if (typeof value == 'string') {
          value = {
            '*': value
          };
        }

        // Convert styles into a rule list
        each(value, function (value, key) {
          styles[key] = styles[key.toUpperCase()] = mode == 'map' ? makeMap(value, /[, ]/) : explode(value, /[, ]/);
        });
      }

      return styles;
    };

    /**
     * Constructs a new Schema instance.
     *
     * @constructor
     * @method Schema
     * @param {Object} settings Name/value settings object.
     */
    return function (settings) {
      var self = this, elements = {}, children = {}, patternElements = [], validStyles, invalidStyles, schemaItems;
      var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap, validClasses;
      var blockElementsMap, nonEmptyElementsMap, moveCaretBeforeOnEnterElementsMap, textBlockElementsMap, textInlineElementsMap;
      var customElementsMap = {}, specialElements = {};

      // Creates an lookup table map object for the specified option or the default value
      var createLookupTable = function (option, defaultValue, extendWith) {
        var value = settings[option];

        if (!value) {
          // Get cached default map or make it if needed
          value = mapCache[option];

          if (!value) {
            value = makeMap(defaultValue, ' ', makeMap(defaultValue.toUpperCase(), ' '));
            value = extend(value, extendWith);

            mapCache[option] = value;
          }
        } else {
          // Create custom map
          value = makeMap(value, /[, ]/, makeMap(value.toUpperCase(), /[, ]/));
        }

        return value;
      };

      settings = settings || {};
      schemaItems = compileSchema(settings.schema);

      // Allow all elements and attributes if verify_html is set to false
      if (settings.verify_html === false) {
        settings.valid_elements = '*[*]';
      }

      validStyles = compileElementMap(settings.valid_styles);
      invalidStyles = compileElementMap(settings.invalid_styles, 'map');
      validClasses = compileElementMap(settings.valid_classes, 'map');

      // Setup map objects
      whiteSpaceElementsMap = createLookupTable(
        'whitespace_elements',
        'pre script noscript style textarea video audio iframe object code'
      );
      selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr');
      shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link ' +
        'meta param embed source wbr track');
      boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize ' +
        'noshade nowrap readonly selected autoplay loop controls');
      nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object ' +
        'script pre code', shortEndedElementsMap);
      moveCaretBeforeOnEnterElementsMap = createLookupTable('move_caret_before_on_enter_elements', 'table', nonEmptyElementsMap);
      textBlockElementsMap = createLookupTable('text_block_elements', 'h1 h2 h3 h4 h5 h6 p div address pre form ' +
        'blockquote center dir fieldset header footer article section hgroup aside nav figure');
      blockElementsMap = createLookupTable('block_elements', 'hr table tbody thead tfoot ' +
        'th tr td li ol ul caption dl dt dd noscript menu isindex option ' +
        'datalist select optgroup figcaption', textBlockElementsMap);
      textInlineElementsMap = createLookupTable('text_inline_elements', 'span strong b em i font strike u var cite ' +
        'dfn code mark q sup sub samp');

      each((settings.special || 'script noscript noframes noembed title style textarea xmp').split(' '), function (name) {
        specialElements[name] = new RegExp('<\/' + name + '[^>]*>', 'gi');
      });

      // Converts a wildcard expression string to a regexp for example *a will become /.*a/.
      var patternToRegExp = function (str) {
        return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$');
      };

      // Parses the specified valid_elements string and adds to the current rules
      // This function is a bit hard to read since it's heavily optimized for speed
      var addValidElements = function (validElements) {
        var ei, el, ai, al, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder,
          prefix, outputName, globalAttributes, globalAttributesOrder, key, value,
          elementRuleRegExp = /^([#+\-])?([^\[!\/]+)(?:\/([^\[!]+))?(?:(!?)\[([^\]]+)\])?$/,
          attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/,
          hasPatternsRegExp = /[*?+]/;

        if (validElements) {
          // Split valid elements into an array with rules
          validElements = split(validElements, ',');

          if (elements['@']) {
            globalAttributes = elements['@'].attributes;
            globalAttributesOrder = elements['@'].attributesOrder;
          }

          // Loop all rules
          for (ei = 0, el = validElements.length; ei < el; ei++) {
            // Parse element rule
            matches = elementRuleRegExp.exec(validElements[ei]);
            if (matches) {
              // Setup local names for matches
              prefix = matches[1];
              elementName = matches[2];
              outputName = matches[3];
              attrData = matches[5];

              // Create new attributes and attributesOrder
              attributes = {};
              attributesOrder = [];

              // Create the new element
              element = {
                attributes: attributes,
                attributesOrder: attributesOrder
              };

              // Padd empty elements prefix
              if (prefix === '#') {
                element.paddEmpty = true;
              }

              // Remove empty elements prefix
              if (prefix === '-') {
                element.removeEmpty = true;
              }

              if (matches[4] === '!') {
                element.removeEmptyAttrs = true;
              }

              // Copy attributes from global rule into current rule
              if (globalAttributes) {
                for (key in globalAttributes) {
                  attributes[key] = globalAttributes[key];
                }

                attributesOrder.push.apply(attributesOrder, globalAttributesOrder);
              }

              // Attributes defined
              if (attrData) {
                attrData = split(attrData, '|');
                for (ai = 0, al = attrData.length; ai < al; ai++) {
                  matches = attrRuleRegExp.exec(attrData[ai]);
                  if (matches) {
                    attr = {};
                    attrType = matches[1];
                    attrName = matches[2].replace(/::/g, ':');
                    prefix = matches[3];
                    value = matches[4];

                    // Required
                    if (attrType === '!') {
                      element.attributesRequired = element.attributesRequired || [];
                      element.attributesRequired.push(attrName);
                      attr.required = true;
                    }

                    // Denied from global
                    if (attrType === '-') {
                      delete attributes[attrName];
                      attributesOrder.splice(inArray(attributesOrder, attrName), 1);
                      continue;
                    }

                    // Default value
                    if (prefix) {
                      // Default value
                      if (prefix === '=') {
                        element.attributesDefault = element.attributesDefault || [];
                        element.attributesDefault.push({ name: attrName, value: value });
                        attr.defaultValue = value;
                      }

                      // Forced value
                      if (prefix === ':') {
                        element.attributesForced = element.attributesForced || [];
                        element.attributesForced.push({ name: attrName, value: value });
                        attr.forcedValue = value;
                      }

                      // Required values
                      if (prefix === '<') {
                        attr.validValues = makeMap(value, '?');
                      }
                    }

                    // Check for attribute patterns
                    if (hasPatternsRegExp.test(attrName)) {
                      element.attributePatterns = element.attributePatterns || [];
                      attr.pattern = patternToRegExp(attrName);
                      element.attributePatterns.push(attr);
                    } else {
                      // Add attribute to order list if it doesn't already exist
                      if (!attributes[attrName]) {
                        attributesOrder.push(attrName);
                      }

                      attributes[attrName] = attr;
                    }
                  }
                }
              }

              // Global rule, store away these for later usage
              if (!globalAttributes && elementName == '@') {
                globalAttributes = attributes;
                globalAttributesOrder = attributesOrder;
              }

              // Handle substitute elements such as b/strong
              if (outputName) {
                element.outputName = elementName;
                elements[outputName] = element;
              }

              // Add pattern or exact element
              if (hasPatternsRegExp.test(elementName)) {
                element.pattern = patternToRegExp(elementName);
                patternElements.push(element);
              } else {
                elements[elementName] = element;
              }
            }
          }
        }
      };

      var setValidElements = function (validElements) {
        elements = {};
        patternElements = [];

        addValidElements(validElements);

        each(schemaItems, function (element, name) {
          children[name] = element.children;
        });
      };

      // Adds custom non HTML elements to the schema
      var addCustomElements = function (customElements) {
        var customElementRegExp = /^(~)?(.+)$/;

        if (customElements) {
          // Flush cached items since we are altering the default maps
          mapCache.text_block_elements = mapCache.block_elements = null;

          each(split(customElements, ','), function (rule) {
            var matches = customElementRegExp.exec(rule),
              inline = matches[1] === '~',
              cloneName = inline ? 'span' : 'div',
              name = matches[2];

            children[name] = children[cloneName];
            customElementsMap[name] = cloneName;

            // If it's not marked as inline then add it to valid block elements
            if (!inline) {
              blockElementsMap[name.toUpperCase()] = {};
              blockElementsMap[name] = {};
            }

            // Add elements clone if needed
            if (!elements[name]) {
              var customRule = elements[cloneName];

              customRule = extend({}, customRule);
              delete customRule.removeEmptyAttrs;
              delete customRule.removeEmpty;

              elements[name] = customRule;
            }

            // Add custom elements at span/div positions
            each(children, function (element, elmName) {
              if (element[cloneName]) {
                children[elmName] = element = extend({}, children[elmName]);
                element[name] = element[cloneName];
              }
            });
          });
        }
      };

      // Adds valid children to the schema object
      var addValidChildren = function (validChildren) {
        var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/;

        // Invalidate the schema cache if the schema is mutated
        mapCache[settings.schema] = null;

        if (validChildren) {
          each(split(validChildren, ','), function (rule) {
            var matches = childRuleRegExp.exec(rule), parent, prefix;

            if (matches) {
              prefix = matches[1];

              // Add/remove items from default
              if (prefix) {
                parent = children[matches[2]];
              } else {
                parent = children[matches[2]] = { '#comment': {} };
              }

              parent = children[matches[2]];

              each(split(matches[3], '|'), function (child) {
                if (prefix === '-') {
                  delete parent[child];
                } else {
                  parent[child] = {};
                }
              });
            }
          });
        }
      };

      var getElementRule = function (name) {
        var element = elements[name], i;

        // Exact match found
        if (element) {
          return element;
        }

        // No exact match then try the patterns
        i = patternElements.length;
        while (i--) {
          element = patternElements[i];

          if (element.pattern.test(name)) {
            return element;
          }
        }
      };

      if (!settings.valid_elements) {
        // No valid elements defined then clone the elements from the schema spec
        each(schemaItems, function (element, name) {
          elements[name] = {
            attributes: element.attributes,
            attributesOrder: element.attributesOrder
          };

          children[name] = element.children;
        });

        // Switch these on HTML4
        if (settings.schema != "html5") {
          each(split('strong/b em/i'), function (item) {
            item = split(item, '/');
            elements[item[1]].outputName = item[0];
          });
        }

        // Add default alt attribute for images, removed since alt="" is treated as presentational.
        // elements.img.attributesDefault = [{name: 'alt', value: ''}];

        // Remove these if they are empty by default
        each(split('ol ul sub sup blockquote span font a table tbody tr strong em b i'), function (name) {
          if (elements[name]) {
            elements[name].removeEmpty = true;
          }
        });

        // Padd these by default
        each(split('p h1 h2 h3 h4 h5 h6 th td pre div address caption'), function (name) {
          elements[name].paddEmpty = true;
        });

        // Remove these if they have no attributes
        each(split('span'), function (name) {
          elements[name].removeEmptyAttrs = true;
        });

        // Remove these by default
        // TODO: Reenable in 4.1
        /*each(split('script style'), function(name) {
         delete elements[name];
         });*/
      } else {
        setValidElements(settings.valid_elements);
      }

      addCustomElements(settings.custom_elements);
      addValidChildren(settings.valid_children);
      addValidElements(settings.extended_valid_elements);

      // Todo: Remove this when we fix list handling to be valid
      addValidChildren('+ol[ul|ol],+ul[ul|ol]');


      // Some elements are not valid by themselves - require parents
      each({
        dd: 'dl',
        dt: 'dl',
        li: 'ul ol',
        td: 'tr',
        th: 'tr',
        tr: 'tbody thead tfoot',
        tbody: 'table',
        thead: 'table',
        tfoot: 'table',
        legend: 'fieldset',
        area: 'map',
        param: 'video audio object'
      }, function (parents, item) {
        if (elements[item]) {
          elements[item].parentsRequired = split(parents);
        }
      });


      // Delete invalid elements
      if (settings.invalid_elements) {
        each(explode(settings.invalid_elements), function (item) {
          if (elements[item]) {
            delete elements[item];
          }
        });
      }

      // If the user didn't allow span only allow internal spans
      if (!getElementRule('span')) {
        addValidElements('span[!data-mce-type|*]');
      }

      /**
       * Name/value map object with valid parents and children to those parents.
       *
       * @example
       * children = {
       *    div:{p:{}, h1:{}}
       * };
       * @field children
       * @type Object
       */
      self.children = children;

      /**
       * Name/value map object with valid styles for each element.
       *
       * @method getValidStyles
       * @type Object
       */
      self.getValidStyles = function () {
        return validStyles;
      };

      /**
       * Name/value map object with valid styles for each element.
       *
       * @method getInvalidStyles
       * @type Object
       */
      self.getInvalidStyles = function () {
        return invalidStyles;
      };

      /**
       * Name/value map object with valid classes for each element.
       *
       * @method getValidClasses
       * @type Object
       */
      self.getValidClasses = function () {
        return validClasses;
      };

      /**
       * Returns a map with boolean attributes.
       *
       * @method getBoolAttrs
       * @return {Object} Name/value lookup map for boolean attributes.
       */
      self.getBoolAttrs = function () {
        return boolAttrMap;
      };

      /**
       * Returns a map with block elements.
       *
       * @method getBlockElements
       * @return {Object} Name/value lookup map for block elements.
       */
      self.getBlockElements = function () {
        return blockElementsMap;
      };

      /**
       * Returns a map with text block elements. Such as: p,h1-h6,div,address
       *
       * @method getTextBlockElements
       * @return {Object} Name/value lookup map for block elements.
       */
      self.getTextBlockElements = function () {
        return textBlockElementsMap;
      };

      /**
       * Returns a map of inline text format nodes for example strong/span or ins.
       *
       * @method getTextInlineElements
       * @return {Object} Name/value lookup map for text format elements.
       */
      self.getTextInlineElements = function () {
        return textInlineElementsMap;
      };

      /**
       * Returns a map with short ended elements such as BR or IMG.
       *
       * @method getShortEndedElements
       * @return {Object} Name/value lookup map for short ended elements.
       */
      self.getShortEndedElements = function () {
        return shortEndedElementsMap;
      };

      /**
       * Returns a map with self closing tags such as <li>.
       *
       * @method getSelfClosingElements
       * @return {Object} Name/value lookup map for self closing tags elements.
       */
      self.getSelfClosingElements = function () {
        return selfClosingElementsMap;
      };

      /**
       * Returns a map with elements that should be treated as contents regardless if it has text
       * content in them or not such as TD, VIDEO or IMG.
       *
       * @method getNonEmptyElements
       * @return {Object} Name/value lookup map for non empty elements.
       */
      self.getNonEmptyElements = function () {
        return nonEmptyElementsMap;
      };

      /**
       * Returns a map with elements that the caret should be moved in front of after enter is
       * pressed
       *
       * @method getMoveCaretBeforeOnEnterElements
       * @return {Object} Name/value lookup map for elements to place the caret in front of.
       */
      self.getMoveCaretBeforeOnEnterElements = function () {
        return moveCaretBeforeOnEnterElementsMap;
      };

      /**
       * Returns a map with elements where white space is to be preserved like PRE or SCRIPT.
       *
       * @method getWhiteSpaceElements
       * @return {Object} Name/value lookup map for white space elements.
       */
      self.getWhiteSpaceElements = function () {
        return whiteSpaceElementsMap;
      };

      /**
       * Returns a map with special elements. These are elements that needs to be parsed
       * in a special way such as script, style, textarea etc. The map object values
       * are regexps used to find the end of the element.
       *
       * @method getSpecialElements
       * @return {Object} Name/value lookup map for special elements.
       */
      self.getSpecialElements = function () {
        return specialElements;
      };

      /**
       * Returns true/false if the specified element and it's child is valid or not
       * according to the schema.
       *
       * @method isValidChild
       * @param {String} name Element name to check for.
       * @param {String} child Element child to verify.
       * @return {Boolean} True/false if the element is a valid child of the specified parent.
       */
      self.isValidChild = function (name, child) {
        var parent = children[name.toLowerCase()];

        return !!(parent && parent[child.toLowerCase()]);
      };

      /**
       * Returns true/false if the specified element name and optional attribute is
       * valid according to the schema.
       *
       * @method isValid
       * @param {String} name Name of element to check.
       * @param {String} attr Optional attribute name to check for.
       * @return {Boolean} True/false if the element and attribute is valid.
       */
      self.isValid = function (name, attr) {
        var attrPatterns, i, rule = getElementRule(name);

        // Check if it's a valid element
        if (rule) {
          if (attr) {
            // Check if attribute name exists
            if (rule.attributes[attr]) {
              return true;
            }

            // Check if attribute matches a regexp pattern
            attrPatterns = rule.attributePatterns;
            if (attrPatterns) {
              i = attrPatterns.length;
              while (i--) {
                if (attrPatterns[i].pattern.test(name)) {
                  return true;
                }
              }
            }
          } else {
            return true;
          }
        }

        // No match
        return false;
      };

      /**
       * Returns true/false if the specified element is valid or not
       * according to the schema.
       *
       * @method getElementRule
       * @param {String} name Element name to check for.
       * @return {Object} Element object or undefined if the element isn't valid.
       */
      self.getElementRule = getElementRule;

      /**
       * Returns an map object of all custom elements.
       *
       * @method getCustomElements
       * @return {Object} Name/value map object of all custom elements.
       */
      self.getCustomElements = function () {
        return customElementsMap;
      };

      /**
       * Parses a valid elements string and adds it to the schema. The valid elements
       * format is for example "element[attr=default|otherattr]".
       * Existing rules will be replaced with the ones specified, so this extends the schema.
       *
       * @method addValidElements
       * @param {String} valid_elements String in the valid elements format to be parsed.
       */
      self.addValidElements = addValidElements;

      /**
       * Parses a valid elements string and sets it to the schema. The valid elements
       * format is for example "element[attr=default|otherattr]".
       * Existing rules will be replaced with the ones specified, so this extends the schema.
       *
       * @method setValidElements
       * @param {String} valid_elements String in the valid elements format to be parsed.
       */
      self.setValidElements = setValidElements;

      /**
       * Adds custom non HTML elements to the schema.
       *
       * @method addCustomElements
       * @param {String} custom_elements Comma separated list of custom elements to add.
       */
      self.addCustomElements = addCustomElements;

      /**
       * Parses a valid children string and adds them to the schema structure. The valid children
       * format is for example: "element[child1|child2]".
       *
       * @method addValidChildren
       * @param {String} valid_children Valid children elements string to parse
       */
      self.addValidChildren = addValidChildren;

      self.elements = elements;
    };
  }
);

/**
 * Styles.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
 */

/**
 * This class is used to parse CSS styles it also compresses styles to reduce the output size.
 *
 * @example
 * var Styles = new tinymce.html.Styles({
 *    url_converter: function(url) {
 *       return url;
 *    }
 * });
 *
 * styles = Styles.parse('border: 1px solid red');
 * styles.color = 'red';
 *
 * console.log(new tinymce.html.StyleSerializer().serialize(styles));
 *
 * @class tinymce.html.Styles
 * @version 3.4
 */
define(
  'tinymce.core.html.Styles',
  [
  ],
  function () {
    return function (settings, schema) {
      /*jshint maxlen:255 */
      /*eslint max-len:0 */
      var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi,
        urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi,
        styleRegExp = /\s*([^:]+):\s*([^;]+);?/g,
        trimRightRegExp = /\s+$/,
        i, encodingLookup = {}, encodingItems, validStyles, invalidStyles, invisibleChar = '\uFEFF';

      settings = settings || {};

      if (schema) {
        validStyles = schema.getValidStyles();
        invalidStyles = schema.getInvalidStyles();
      }

      encodingItems = ('\\" \\\' \\; \\: ; : ' + invisibleChar).split(' ');
      for (i = 0; i < encodingItems.length; i++) {
        encodingLookup[encodingItems[i]] = invisibleChar + i;
        encodingLookup[invisibleChar + i] = encodingItems[i];
      }

      var toHex = function (match, r, g, b) {
        var hex = function (val) {
          val = parseInt(val, 10).toString(16);

          return val.length > 1 ? val : '0' + val; // 0 -> 00
        };

        return '#' + hex(r) + hex(g) + hex(b);
      };

      return {
        /**
         * Parses the specified RGB color value and returns a hex version of that color.
         *
         * @method toHex
         * @param {String} color RGB string value like rgb(1,2,3)
         * @return {String} Hex version of that RGB value like #FF00FF.
         */
        toHex: function (color) {
          return color.replace(rgbRegExp, toHex);
        },

        /**
         * Parses the specified style value into an object collection. This parser will also
         * merge and remove any redundant items that browsers might have added. It will also convert non hex
         * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings.
         *
         * @method parse
         * @param {String} css Style value to parse for example: border:1px solid red;.
         * @return {Object} Object representation of that style like {border: '1px solid red'}
         */
        parse: function (css) {
          var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter;
          var urlConverterScope = settings.url_converter_scope || this;

          var compress = function (prefix, suffix, noJoin) {
            var top, right, bottom, left;

            top = styles[prefix + '-top' + suffix];
            if (!top) {
              return;
            }

            right = styles[prefix + '-right' + suffix];
            if (!right) {
              return;
            }

            bottom = styles[prefix + '-bottom' + suffix];
            if (!bottom) {
              return;
            }

            left = styles[prefix + '-left' + suffix];
            if (!left) {
              return;
            }

            var box = [top, right, bottom, left];
            i = box.length - 1;
            while (i--) {
              if (box[i] !== box[i + 1]) {
                break;
              }
            }

            if (i > -1 && noJoin) {
              return;
            }

            styles[prefix + suffix] = i == -1 ? box[0] : box.join(' ');
            delete styles[prefix + '-top' + suffix];
            delete styles[prefix + '-right' + suffix];
            delete styles[prefix + '-bottom' + suffix];
            delete styles[prefix + '-left' + suffix];
          };

          /**
           * Checks if the specific style can be compressed in other words if all border-width are equal.
           */
          var canCompress = function (key) {
            var value = styles[key], i;

            if (!value) {
              return;
            }

            value = value.split(' ');
            i = value.length;
            while (i--) {
              if (value[i] !== value[0]) {
                return false;
              }
            }

            styles[key] = value[0];

            return true;
          };

          /**
           * Compresses multiple styles into one style.
           */
          var compress2 = function (target, a, b, c) {
            if (!canCompress(a)) {
              return;
            }

            if (!canCompress(b)) {
              return;
            }

            if (!canCompress(c)) {
              return;
            }

            // Compress
            styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c];
            delete styles[a];
            delete styles[b];
            delete styles[c];
          };

          // Encodes the specified string by replacing all \" \' ; : with _<num>
          var encode = function (str) {
            isEncoded = true;

            return encodingLookup[str];
          };

          // Decodes the specified string by replacing all _<num> with it's original value \" \' etc
          // It will also decode the \" \' if keepSlashes is set to fale or omitted
          var decode = function (str, keepSlashes) {
            if (isEncoded) {
              str = str.replace(/\uFEFF[0-9]/g, function (str) {
                return encodingLookup[str];
              });
            }

            if (!keepSlashes) {
              str = str.replace(/\\([\'\";:])/g, "$1");
            }

            return str;
          };

          var decodeSingleHexSequence = function (escSeq) {
            return String.fromCharCode(parseInt(escSeq.slice(1), 16));
          };

          var decodeHexSequences = function (value) {
            return value.replace(/\\[0-9a-f]+/gi, decodeSingleHexSequence);
          };

          var processUrl = function (match, url, url2, url3, str, str2) {
            str = str || str2;

            if (str) {
              str = decode(str);

              // Force strings into single quote format
              return "'" + str.replace(/\'/g, "\\'") + "'";
            }

            url = decode(url || url2 || url3);

            if (!settings.allow_script_urls) {
              var scriptUrl = url.replace(/[\s\r\n]+/g, '');

              if (/(java|vb)script:/i.test(scriptUrl)) {
                return "";
              }

              if (!settings.allow_svg_data_urls && /^data:image\/svg/i.test(scriptUrl)) {
                return "";
              }
            }

            // Convert the URL to relative/absolute depending on config
            if (urlConverter) {
              url = urlConverter.call(urlConverterScope, url, 'style');
            }

            // Output new URL format
            return "url('" + url.replace(/\'/g, "\\'") + "')";
          };

          if (css) {
            css = css.replace(/[\u0000-\u001F]/g, '');

            // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing
            css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function (str) {
              return str.replace(/[;:]/g, encode);
            });

            // Parse styles
            while ((matches = styleRegExp.exec(css))) {
              styleRegExp.lastIndex = matches.index + matches[0].length;
              name = matches[1].replace(trimRightRegExp, '').toLowerCase();
              value = matches[2].replace(trimRightRegExp, '');

              if (name && value) {
                // Decode escaped sequences like \65 -> e
                name = decodeHexSequences(name);
                value = decodeHexSequences(value);

                // Skip properties with double quotes and sequences like \" \' in their names
                // See 'mXSS Attacks: Attacking well-secured Web-Applications by using innerHTML Mutations'
                // https://cure53.de/fp170.pdf
                if (name.indexOf(invisibleChar) !== -1 || name.indexOf('"') !== -1) {
                  continue;
                }

                // Don't allow behavior name or expression/comments within the values
                if (!settings.allow_script_urls && (name == "behavior" || /expression\s*\(|\/\*|\*\//.test(value))) {
                  continue;
                }

                // Opera will produce 700 instead of bold in their style values
                if (name === 'font-weight' && value === '700') {
                  value = 'bold';
                } else if (name === 'color' || name === 'background-color') { // Lowercase colors like RED
                  value = value.toLowerCase();
                }

                // Convert RGB colors to HEX
                value = value.replace(rgbRegExp, toHex);

                // Convert URLs and force them into url('value') format
                value = value.replace(urlOrStrRegExp, processUrl);
                styles[name] = isEncoded ? decode(value, true) : value;
              }
            }
            // Compress the styles to reduce it's size for example IE will expand styles
            compress("border", "", true);
            compress("border", "-width");
            compress("border", "-color");
            compress("border", "-style");
            compress("padding", "");
            compress("margin", "");
            compress2('border', 'border-width', 'border-style', 'border-color');

            // Remove pointless border, IE produces these
            if (styles.border === 'medium none') {
              delete styles.border;
            }

            // IE 11 will produce a border-image: none when getting the style attribute from <p style="border: 1px solid red"></p>
            // So let us assume it shouldn't be there
            if (styles['border-image'] === 'none') {
              delete styles['border-image'];
            }
          }

          return styles;
        },

        /**
         * Serializes the specified style object into a string.
         *
         * @method serialize
         * @param {Object} styles Object to serialize as string for example: {border: '1px solid red'}
         * @param {String} elementName Optional element name, if specified only the styles that matches the schema will be serialized.
         * @return {String} String representation of the style object for example: border: 1px solid red.
         */
        serialize: function (styles, elementName) {
          var css = '', name, value;

          var serializeStyles = function (name) {
            var styleList, i, l, value;

            styleList = validStyles[name];
            if (styleList) {
              for (i = 0, l = styleList.length; i < l; i++) {
                name = styleList[i];
                value = styles[name];

                if (value) {
                  css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
                }
              }
            }
          };

          var isValid = function (name, elementName) {
            var styleMap;

            styleMap = invalidStyles['*'];
            if (styleMap && styleMap[name]) {
              return false;
            }

            styleMap = invalidStyles[elementName];
            if (styleMap && styleMap[name]) {
              return false;
            }

            return true;
          };

          // Serialize styles according to schema
          if (elementName && validStyles) {
            // Serialize global styles and element specific styles
            serializeStyles('*');
            serializeStyles(elementName);
          } else {
            // Output the styles in the order they are inside the object
            for (name in styles) {
              value = styles[name];

              if (value && (!invalidStyles || isValid(name, elementName))) {
                css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
              }
            }
          }

          return css;
        }
      };
    };
  }
);

/**
 * DOMUtils.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
 */

/**
 * Utility class for various DOM manipulation and retrieval functions.
 *
 * @class tinymce.dom.DOMUtils
 * @example
 * // Add a class to an element by id in the page
 * tinymce.DOM.addClass('someid', 'someclass');
 *
 * // Add a class to an element by id inside the editor
 * tinymce.activeEditor.dom.addClass('someid', 'someclass');
 */
define(
  'tinymce.core.dom.DOMUtils',
  [
    'global!document',
    'global!window',
    'tinymce.core.dom.DomQuery',
    'tinymce.core.dom.EventUtils',
    'tinymce.core.dom.Sizzle',
    'tinymce.core.dom.StyleSheetLoader',
    'tinymce.core.dom.TreeWalker',
    'tinymce.core.Env',
    'tinymce.core.html.Entities',
    'tinymce.core.html.Schema',
    'tinymce.core.html.Styles',
    'tinymce.core.util.Tools'
  ],
  function (document, window, DomQuery, EventUtils, Sizzle, StyleSheetLoader, TreeWalker, Env, Entities, Schema, Styles, Tools) {
    // Shorten names
    var each = Tools.each, is = Tools.is, grep = Tools.grep, trim = Tools.trim;
    var isIE = Env.ie;
    var simpleSelectorRe = /^([a-z0-9],?)+$/i;
    var whiteSpaceRegExp = /^[ \t\r\n]*$/;

    var setupAttrHooks = function (domUtils, settings) {
      var attrHooks = {}, keepValues = settings.keep_values, keepUrlHook;

      keepUrlHook = {
        set: function ($elm, value, name) {
          if (settings.url_converter) {
            value = settings.url_converter.call(settings.url_converter_scope || domUtils, value, name, $elm[0]);
          }

          $elm.attr('data-mce-' + name, value).attr(name, value);
        },

        get: function ($elm, name) {
          return $elm.attr('data-mce-' + name) || $elm.attr(name);
        }
      };

      attrHooks = {
        style: {
          set: function ($elm, value) {
            if (value !== null && typeof value === 'object') {
              $elm.css(value);
              return;
            }

            if (keepValues) {
              $elm.attr('data-mce-style', value);
            }

            $elm.attr('style', value);
          },

          get: function ($elm) {
            var value = $elm.attr('data-mce-style') || $elm.attr('style');

            value = domUtils.serializeStyle(domUtils.parseStyle(value), $elm[0].nodeName);

            return value;
          }
        }
      };

      if (keepValues) {
        attrHooks.href = attrHooks.src = keepUrlHook;
      }

      return attrHooks;
    };

    var updateInternalStyleAttr = function (domUtils, $elm) {
      var value = $elm.attr('style');

      value = domUtils.serializeStyle(domUtils.parseStyle(value), $elm[0].nodeName);

      if (!value) {
        value = null;
      }

      $elm.attr('data-mce-style', value);
    };

    var nodeIndex = function (node, normalized) {
      var idx = 0, lastNodeType, nodeType;

      if (node) {
        for (lastNodeType = node.nodeType, node = node.previousSibling; node; node = node.previousSibling) {
          nodeType = node.nodeType;

          // Normalize text nodes
          if (normalized && nodeType == 3) {
            if (nodeType == lastNodeType || !node.nodeValue.length) {
              continue;
            }
          }
          idx++;
          lastNodeType = nodeType;
        }
      }

      return idx;
    };

    /**
     * Constructs a new DOMUtils instance. Consult the Wiki for more details on settings etc for this class.
     *
     * @constructor
     * @method DOMUtils
     * @param {Document} doc Document reference to bind the utility class to.
     * @param {settings} settings Optional settings collection.
     */
    var DOMUtils = function (doc, settings) {
      var self = this, blockElementsMap;

      self.doc = doc;
      self.win = window;
      self.files = {};
      self.counter = 0;
      self.stdMode = !isIE || doc.documentMode >= 8;
      self.boxModel = !isIE || doc.compatMode == "CSS1Compat" || self.stdMode;
      self.styleSheetLoader = new StyleSheetLoader(doc);
      self.boundEvents = [];
      self.settings = settings = settings || {};
      self.schema = settings.schema ? settings.schema : new Schema({});
      self.styles = new Styles({
        url_converter: settings.url_converter,
        url_converter_scope: settings.url_converter_scope
      }, settings.schema);

      self.fixDoc(doc);
      self.events = settings.ownEvents ? new EventUtils(settings.proxy) : EventUtils.Event;
      self.attrHooks = setupAttrHooks(self, settings);
      blockElementsMap = settings.schema ? settings.schema.getBlockElements() : {};
      self.$ = DomQuery.overrideDefaults(function () {
        return {
          context: doc,
          element: self.getRoot()
        };
      });

      /**
       * Returns true/false if the specified element is a block element or not.
       *
       * @method isBlock
       * @param {Node/String} node Element/Node to check.
       * @return {Boolean} True/False state if the node is a block element or not.
       */
      self.isBlock = function (node) {
        // Fix for #5446
        if (!node) {
          return false;
        }

        // This function is called in module pattern style since it might be executed with the wrong this scope
        var type = node.nodeType;

        // If it's a node then check the type and use the nodeName
        if (type) {
          return !!(type === 1 && blockElementsMap[node.nodeName]);
        }

        return !!blockElementsMap[node];
      };
    };

    DOMUtils.prototype = {
      $$: function (elm) {
        if (typeof elm == 'string') {
          elm = this.get(elm);
        }

        return this.$(elm);
      },

      root: null,

      fixDoc: function (doc) {
        var settings = this.settings, name;

        if (isIE && settings.schema) {
          // Add missing HTML 4/5 elements to IE
          ('abbr article aside audio canvas ' +
            'details figcaption figure footer ' +
            'header hgroup mark menu meter nav ' +
            'output progress section summary ' +
            'time video').replace(/\w+/g, function (name) {
              doc.createElement(name);
            });

          // Create all custom elements
          for (name in settings.schema.getCustomElements()) {
            doc.createElement(name);
          }
        }
      },

      clone: function (node, deep) {
        var self = this, clone, doc;

        // TODO: Add feature detection here in the future
        if (!isIE || node.nodeType !== 1 || deep) {
          return node.cloneNode(deep);
        }

        doc = self.doc;

        // Make a HTML5 safe shallow copy
        if (!deep) {
          clone = doc.createElement(node.nodeName);

          // Copy attribs
          each(self.getAttribs(node), function (attr) {
            self.setAttrib(clone, attr.nodeName, self.getAttrib(node, attr.nodeName));
          });

          return clone;
        }

        return clone.firstChild;
      },

      /**
       * Returns the root node of the document. This is normally the body but might be a DIV. Parents like getParent will not
       * go above the point of this root node.
       *
       * @method getRoot
       * @return {Element} Root element for the utility class.
       */
      getRoot: function () {
        var self = this;

        return self.settings.root_element || self.doc.body;
      },

      /**
       * Returns the viewport of the window.
       *
       * @method getViewPort
       * @param {Window} win Optional window to get viewport of.
       * @return {Object} Viewport object with fields x, y, w and h.
       */
      getViewPort: function (win) {
        var doc, rootElm;

        win = !win ? this.win : win;
        doc = win.document;
        rootElm = this.boxModel ? doc.documentElement : doc.body;

        // Returns viewport size excluding scrollbars
        return {
          x: win.pageXOffset || rootElm.scrollLeft,
          y: win.pageYOffset || rootElm.scrollTop,
          w: win.innerWidth || rootElm.clientWidth,
          h: win.innerHeight || rootElm.clientHeight
        };
      },

      /**
       * Returns the rectangle for a specific element.
       *
       * @method getRect
       * @param {Element/String} elm Element object or element ID to get rectangle from.
       * @return {object} Rectangle for specified element object with x, y, w, h fields.
       */
      getRect: function (elm) {
        var self = this, pos, size;

        elm = self.get(elm);
        pos = self.getPos(elm);
        size = self.getSize(elm);

        return {
          x: pos.x, y: pos.y,
          w: size.w, h: size.h
        };
      },

      /**
       * Returns the size dimensions of the specified element.
       *
       * @method getSize
       * @param {Element/String} elm Element object or element ID to get rectangle from.
       * @return {object} Rectangle for specified element object with w, h fields.
       */
      getSize: function (elm) {
        var self = this, w, h;

        elm = self.get(elm);
        w = self.getStyle(elm, 'width');
        h = self.getStyle(elm, 'height');

        // Non pixel value, then force offset/clientWidth
        if (w.indexOf('px') === -1) {
          w = 0;
        }

        // Non pixel value, then force offset/clientWidth
        if (h.indexOf('px') === -1) {
          h = 0;
        }

        return {
          w: parseInt(w, 10) || elm.offsetWidth || elm.clientWidth,
          h: parseInt(h, 10) || elm.offsetHeight || elm.clientHeight
        };
      },

      /**
       * Returns a node by the specified selector function. This function will
       * loop through all parent nodes and call the specified function for each node.
       * If the function then returns true indicating that it has found what it was looking for, the loop execution will then end
       * and the node it found will be returned.
       *
       * @method getParent
       * @param {Node/String} node DOM node to search parents on or ID string.
       * @param {function} selector Selection function or CSS selector to execute on each node.
       * @param {Node} root Optional root element, never go beyond this point.
       * @return {Node} DOM Node or null if it wasn't found.
       */
      getParent: function (node, selector, root) {
        return this.getParents(node, selector, root, false);
      },

      /**
       * Returns a node list of all parents matching the specified selector function or pattern.
       * If the function then returns true indicating that it has found what it was looking for and that node will be collected.
       *
       * @method getParents
       * @param {Node/String} node DOM node to search parents on or ID string.
       * @param {function} selector Selection function to execute on each node or CSS pattern.
       * @param {Node} root Optional root element, never go beyond this point.
       * @return {Array} Array of nodes or null if it wasn't found.
       */
      getParents: function (node, selector, root, collect) {
        var self = this, selectorVal, result = [];

        node = self.get(node);
        collect = collect === undefined;

        // Default root on inline mode
        root = root || (self.getRoot().nodeName != 'BODY' ? self.getRoot().parentNode : null);

        // Wrap node name as func
        if (is(selector, 'string')) {
          selectorVal = selector;

          if (selector === '*') {
            selector = function (node) {
              return node.nodeType == 1;
            };
          } else {
            selector = function (node) {
              return self.is(node, selectorVal);
            };
          }
        }

        while (node) {
          if (node == root || !node.nodeType || node.nodeType === 9) {
            break;
          }

          if (!selector || selector(node)) {
            if (collect) {
              result.push(node);
            } else {
              return node;
            }
          }

          node = node.parentNode;
        }

        return collect ? result : null;
      },

      /**
       * Returns the specified element by ID or the input element if it isn't a string.
       *
       * @method get
       * @param {String/Element} n Element id to look for or element to just pass though.
       * @return {Element} Element matching the specified id or null if it wasn't found.
       */
      get: function (elm) {
        var name;

        if (elm && this.doc && typeof elm == 'string') {
          name = elm;
          elm = this.doc.getElementById(elm);

          // IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick
          if (elm && elm.id !== name) {
            return this.doc.getElementsByName(name)[1];
          }
        }

        return elm;
      },

      /**
       * Returns the next node that matches selector or function
       *
       * @method getNext
       * @param {Node} node Node to find siblings from.
       * @param {String/function} selector Selector CSS expression or function.
       * @return {Node} Next node item matching the selector or null if it wasn't found.
       */
      getNext: function (node, selector) {
        return this._findSib(node, selector, 'nextSibling');
      },

      /**
       * Returns the previous node that matches selector or function
       *
       * @method getPrev
       * @param {Node} node Node to find siblings from.
       * @param {String/function} selector Selector CSS expression or function.
       * @return {Node} Previous node item matching the selector or null if it wasn't found.
       */
      getPrev: function (node, selector) {
        return this._findSib(node, selector, 'previousSibling');
      },

      // #ifndef jquery

      /**
       * Selects specific elements by a CSS level 3 pattern. For example "div#a1 p.test".
       * This function is optimized for the most common patterns needed in TinyMCE but it also performs well enough
       * on more complex patterns.
       *
       * @method select
       * @param {String} selector CSS level 3 pattern to select/find elements by.
       * @param {Object} scope Optional root element/scope element to search in.
       * @return {Array} Array with all matched elements.
       * @example
       * // Adds a class to all paragraphs in the currently active editor
       * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass');
       *
       * // Adds a class to all spans that have the test class in the currently active editor
       * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('span.test'), 'someclass')
       */
      select: function (selector, scope) {
        var self = this;

        /*eslint new-cap:0 */
        return Sizzle(selector, self.get(scope) || self.settings.root_element || self.doc, []);
      },

      /**
       * Returns true/false if the specified element matches the specified css pattern.
       *
       * @method is
       * @param {Node/NodeList} elm DOM node to match or an array of nodes to match.
       * @param {String} selector CSS pattern to match the element against.
       */
      is: function (elm, selector) {
        var i;

        if (!elm) {
          return false;
        }

        // If it isn't an array then try to do some simple selectors instead of Sizzle for to boost performance
        if (elm.length === undefined) {
          // Simple all selector
          if (selector === '*') {
            return elm.nodeType == 1;
          }

          // Simple selector just elements
          if (simpleSelectorRe.test(selector)) {
            selector = selector.toLowerCase().split(/,/);
            elm = elm.nodeName.toLowerCase();

            for (i = selector.length - 1; i >= 0; i--) {
              if (selector[i] == elm) {
                return true;
              }
            }

            return false;
          }
        }

        // Is non element
        if (elm.nodeType && elm.nodeType != 1) {
          return false;
        }

        var elms = elm.nodeType ? [elm] : elm;

        /*eslint new-cap:0 */
        return Sizzle(selector, elms[0].ownerDocument || elms[0], null, elms).length > 0;
      },

      // #endif

      /**
       * Adds the specified element to another element or elements.
       *
       * @method add
       * @param {String/Element/Array} parentElm Element id string, DOM node element or array of ids or elements to add to.
       * @param {String/Element} name Name of new element to add or existing element to add.
       * @param {Object} attrs Optional object collection with arguments to add to the new element(s).
       * @param {String} html Optional inner HTML contents to add for each element.
       * @param {Boolean} create Optional flag if the element should be created or added.
       * @return {Element/Array} Element that got created, or an array of created elements if multiple input elements
       * were passed in.
       * @example
       * // Adds a new paragraph to the end of the active editor
       * tinymce.activeEditor.dom.add(tinymce.activeEditor.getBody(), 'p', {title: 'my title'}, 'Some content');
       */
      add: function (parentElm, name, attrs, html, create) {
        var self = this;

        return this.run(parentElm, function (parentElm) {
          var newElm;

          newElm = is(name, 'string') ? self.doc.createElement(name) : name;
          self.setAttribs(newElm, attrs);

          if (html) {
            if (html.nodeType) {
              newElm.appendChild(html);
            } else {
              self.setHTML(newElm, html);
            }
          }

          return !create ? parentElm.appendChild(newElm) : newElm;
        });
      },

      /**
       * Creates a new element.
       *
       * @method create
       * @param {String} name Name of new element.
       * @param {Object} attrs Optional object name/value collection with element attributes.
       * @param {String} html Optional HTML string to set as inner HTML of the element.
       * @return {Element} HTML DOM node element that got created.
       * @example
       * // Adds an element where the caret/selection is in the active editor
       * var el = tinymce.activeEditor.dom.create('div', {id: 'test', 'class': 'myclass'}, 'some content');
       * tinymce.activeEditor.selection.setNode(el);
       */
      create: function (name, attrs, html) {
        return this.add(this.doc.createElement(name), name, attrs, html, 1);
      },

      /**
       * Creates HTML string for element. The element will be closed unless an empty inner HTML string is passed in.
       *
       * @method createHTML
       * @param {String} name Name of new element.
       * @param {Object} attrs Optional object name/value collection with element attributes.
       * @param {String} html Optional HTML string to set as inner HTML of the element.
       * @return {String} String with new HTML element, for example: <a href="#">test</a>.
       * @example
       * // Creates a html chunk and inserts it at the current selection/caret location
       * tinymce.activeEditor.selection.setContent(tinymce.activeEditor.dom.createHTML('a', {href: 'test.html'}, 'some line'));
       */
      createHTML: function (name, attrs, html) {
        var outHtml = '', key;

        outHtml += '<' + name;

        for (key in attrs) {
          if (attrs.hasOwnProperty(key) && attrs[key] !== null && typeof attrs[key] != 'undefined') {
            outHtml += ' ' + key + '="' + this.encode(attrs[key]) + '"';
          }
        }

        // A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime
        if (typeof html != "undefined") {
          return outHtml + '>' + html + '</' + name + '>';
        }

        return outHtml + ' />';
      },

      /**
       * Creates a document fragment out of the specified HTML string.
       *
       * @method createFragment
       * @param {String} html Html string to create fragment from.
       * @return {DocumentFragment} Document fragment node.
       */
      createFragment: function (html) {
        var frag, node, doc = this.doc, container;

        container = doc.createElement("div");
        frag = doc.createDocumentFragment();

        if (html) {
          container.innerHTML = html;
        }

        while ((node = container.firstChild)) {
          frag.appendChild(node);
        }

        return frag;
      },

      /**
       * Removes/deletes the specified element(s) from the DOM.
       *
       * @method remove
       * @param {String/Element/Array} node ID of element or DOM element object or array containing multiple elements/ids.
       * @param {Boolean} keepChildren Optional state to keep children or not. If set to true all children will be
       * placed at the location of the removed element.
       * @return {Element/Array} HTML DOM element that got removed, or an array of removed elements if multiple input elements
       * were passed in.
       * @example
       * // Removes all paragraphs in the active editor
       * tinymce.activeEditor.dom.remove(tinymce.activeEditor.dom.select('p'));
       *
       * // Removes an element by id in the document
       * tinymce.DOM.remove('mydiv');
       */
      remove: function (node, keepChildren) {
        node = this.$$(node);

        if (keepChildren) {
          node.each(function () {
            var child;

            while ((child = this.firstChild)) {
              if (child.nodeType == 3 && child.data.length === 0) {
                this.removeChild(child);
              } else {
                this.parentNode.insertBefore(child, this);
              }
            }
          }).remove();
        } else {
          node.remove();
        }

        return node.length > 1 ? node.toArray() : node[0];
      },

      /**
       * Sets the CSS style value on a HTML element. The name can be a camelcase string
       * or the CSS style name like background-color.
       *
       * @method setStyle
       * @param {String/Element/Array} elm HTML element/Array of elements to set CSS style value on.
       * @param {String} name Name of the style value to set.
       * @param {String} value Value to set on the style.
       * @example
       * // Sets a style value on all paragraphs in the currently active editor
       * tinymce.activeEditor.dom.setStyle(tinymce.activeEditor.dom.select('p'), 'background-color', 'red');
       *
       * // Sets a style value to an element by id in the current document
       * tinymce.DOM.setStyle('mydiv', 'background-color', 'red');
       */
      setStyle: function (elm, name, value) {
        elm = this.$$(elm).css(name, value);

        if (this.settings.update_styles) {
          updateInternalStyleAttr(this, elm);
        }
      },

      /**
       * Returns the current style or runtime/computed value of an element.
       *
       * @method getStyle
       * @param {String/Element} elm HTML element or element id string to get style from.
       * @param {String} name Style name to return.
       * @param {Boolean} computed Computed style.
       * @return {String} Current style or computed style value of an element.
       */
      getStyle: function (elm, name, computed) {
        elm = this.$$(elm);

        if (computed) {
          return elm.css(name);
        }

        // Camelcase it, if needed
        name = name.replace(/-(\D)/g, function (a, b) {
          return b.toUpperCase();
        });

        if (name == 'float') {
          name = Env.ie && Env.ie < 12 ? 'styleFloat' : 'cssFloat';
        }

        return elm[0] && elm[0].style ? elm[0].style[name] : undefined;
      },

      /**
       * Sets multiple styles on the specified element(s).
       *
       * @method setStyles
       * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set styles on.
       * @param {Object} styles Name/Value collection of style items to add to the element(s).
       * @example
       * // Sets styles on all paragraphs in the currently active editor
       * tinymce.activeEditor.dom.setStyles(tinymce.activeEditor.dom.select('p'), {'background-color': 'red', 'color': 'green'});
       *
       * // Sets styles to an element by id in the current document
       * tinymce.DOM.setStyles('mydiv', {'background-color': 'red', 'color': 'green'});
       */
      setStyles: function (elm, styles) {
        elm = this.$$(elm).css(styles);

        if (this.settings.update_styles) {
          updateInternalStyleAttr(this, elm);
        }
      },

      /**
       * Removes all attributes from an element or elements.
       *
       * @method removeAllAttribs
       * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to remove attributes from.
       */
      removeAllAttribs: function (e) {
        return this.run(e, function (e) {
          var i, attrs = e.attributes;
          for (i = attrs.length - 1; i >= 0; i--) {
            e.removeAttributeNode(attrs.item(i));
          }
        });
      },

      /**
       * Sets the specified attribute of an element or elements.
       *
       * @method setAttrib
       * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attribute on.
       * @param {String} name Name of attribute to set.
       * @param {String} value Value to set on the attribute - if this value is falsy like null, 0 or '' it will remove
       * the attribute instead.
       * @example
       * // Sets class attribute on all paragraphs in the active editor
       * tinymce.activeEditor.dom.setAttrib(tinymce.activeEditor.dom.select('p'), 'class', 'myclass');
       *
       * // Sets class attribute on a specific element in the current page
       * tinymce.dom.setAttrib('mydiv', 'class', 'myclass');
       */
      setAttrib: function (elm, name, value) {
        var self = this, originalValue, hook, settings = self.settings;

        if (value === '') {
          value = null;
        }

        elm = self.$$(elm);
        originalValue = elm.attr(name);

        if (!elm.length) {
          return;
        }

        hook = self.attrHooks[name];
        if (hook && hook.set) {
          hook.set(elm, value, name);
        } else {
          elm.attr(name, value);
        }

        if (originalValue != value && settings.onSetAttrib) {
          settings.onSetAttrib({
            attrElm: elm,
            attrName: name,
            attrValue: value
          });
        }
      },

      /**
       * Sets two or more specified attributes of an element or elements.
       *
       * @method setAttribs
       * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attributes on.
       * @param {Object} attrs Name/Value collection of attribute items to add to the element(s).
       * @example
       * // Sets class and title attributes on all paragraphs in the active editor
       * tinymce.activeEditor.dom.setAttribs(tinymce.activeEditor.dom.select('p'), {'class': 'myclass', title: 'some title'});
       *
       * // Sets class and title attributes on a specific element in the current page
       * tinymce.DOM.setAttribs('mydiv', {'class': 'myclass', title: 'some title'});
       */
      setAttribs: function (elm, attrs) {
        var self = this;

        self.$$(elm).each(function (i, node) {
          each(attrs, function (value, name) {
            self.setAttrib(node, name, value);
          });
        });
      },

      /**
       * Returns the specified attribute by name.
       *
       * @method getAttrib
       * @param {String/Element} elm Element string id or DOM element to get attribute from.
       * @param {String} name Name of attribute to get.
       * @param {String} defaultVal Optional default value to return if the attribute didn't exist.
       * @return {String} Attribute value string, default value or null if the attribute wasn't found.
       */
      getAttrib: function (elm, name, defaultVal) {
        var self = this, hook, value;

        elm = self.$$(elm);

        if (elm.length) {
          hook = self.attrHooks[name];

          if (hook && hook.get) {
            value = hook.get(elm, name);
          } else {
            value = elm.attr(name);
          }
        }

        if (typeof value == 'undefined') {
          value = defaultVal || '';
        }

        return value;
      },

      /**
       * Returns the absolute x, y position of a node. The position will be returned in an object with x, y fields.
       *
       * @method getPos
       * @param {Element/String} elm HTML element or element id to get x, y position from.
       * @param {Element} rootElm Optional root element to stop calculations at.
       * @return {object} Absolute position of the specified element object with x, y fields.
       */
      getPos: function (elm, rootElm) {
        var self = this, x = 0, y = 0, offsetParent, doc = self.doc, body = doc.body, pos;

        elm = self.get(elm);
        rootElm = rootElm || body;

        if (elm) {
          // Use getBoundingClientRect if it exists since it's faster than looping offset nodes
          // Fallback to offsetParent calculations if the body isn't static better since it stops at the body root
          if (rootElm === body && elm.getBoundingClientRect && DomQuery(body).css('position') === 'static') {
            pos = elm.getBoundingClientRect();
            rootElm = self.boxModel ? doc.documentElement : body;

            // Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit
            // Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position
            x = pos.left + (doc.documentElement.scrollLeft || body.scrollLeft) - rootElm.clientLeft;
            y = pos.top + (doc.documentElement.scrollTop || body.scrollTop) - rootElm.clientTop;

            return { x: x, y: y };
          }

          offsetParent = elm;
          while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) {
            x += offsetParent.offsetLeft || 0;
            y += offsetParent.offsetTop || 0;
            offsetParent = offsetParent.offsetParent;
          }

          offsetParent = elm.parentNode;
          while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) {
            x -= offsetParent.scrollLeft || 0;
            y -= offsetParent.scrollTop || 0;
            offsetParent = offsetParent.parentNode;
          }
        }

        return { x: x, y: y };
      },

      /**
       * Parses the specified style value into an object collection. This parser will also
       * merge and remove any redundant items that browsers might have added. It will also convert non-hex
       * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings.
       *
       * @method parseStyle
       * @param {String} cssText Style value to parse, for example: border:1px solid red;.
       * @return {Object} Object representation of that style, for example: {border: '1px solid red'}
       */
      parseStyle: function (cssText) {
        return this.styles.parse(cssText);
      },

      /**
       * Serializes the specified style object into a string.
       *
       * @method serializeStyle
       * @param {Object} styles Object to serialize as string, for example: {border: '1px solid red'}
       * @param {String} name Optional element name.
       * @return {String} String representation of the style object, for example: border: 1px solid red.
       */
      serializeStyle: function (styles, name) {
        return this.styles.serialize(styles, name);
      },

      /**
       * Adds a style element at the top of the document with the specified cssText content.
       *
       * @method addStyle
       * @param {String} cssText CSS Text style to add to top of head of document.
       */
      addStyle: function (cssText) {
        var self = this, doc = self.doc, head, styleElm;

        // Prevent inline from loading the same styles twice
        if (self !== DOMUtils.DOM && doc === document) {
          var addedStyles = DOMUtils.DOM.addedStyles;

          addedStyles = addedStyles || [];
          if (addedStyles[cssText]) {
            return;
          }

          addedStyles[cssText] = true;
          DOMUtils.DOM.addedStyles = addedStyles;
        }

        // Create style element if needed
        styleElm = doc.getElementById('mceDefaultStyles');
        if (!styleElm) {
          styleElm = doc.createElement('style');
          styleElm.id = 'mceDefaultStyles';
          styleElm.type = 'text/css';

          head = doc.getElementsByTagName('head')[0];
          if (head.firstChild) {
            head.insertBefore(styleElm, head.firstChild);
          } else {
            head.appendChild(styleElm);
          }
        }

        // Append style data to old or new style element
        if (styleElm.styleSheet) {
          styleElm.styleSheet.cssText += cssText;
        } else {
          styleElm.appendChild(doc.createTextNode(cssText));
        }
      },

      /**
       * Imports/loads the specified CSS file into the document bound to the class.
       *
       * @method loadCSS
       * @param {String} url URL to CSS file to load.
       * @example
       * // Loads a CSS file dynamically into the current document
       * tinymce.DOM.loadCSS('somepath/some.css');
       *
       * // Loads a CSS file into the currently active editor instance
       * tinymce.activeEditor.dom.loadCSS('somepath/some.css');
       *
       * // Loads a CSS file into an editor instance by id
       * tinymce.get('someid').dom.loadCSS('somepath/some.css');
       *
       * // Loads multiple CSS files into the current document
       * tinymce.DOM.loadCSS('somepath/some.css,somepath/someother.css');
       */
      loadCSS: function (url) {
        var self = this, doc = self.doc, head;

        // Prevent inline from loading the same CSS file twice
        if (self !== DOMUtils.DOM && doc === document) {
          DOMUtils.DOM.loadCSS(url);
          return;
        }

        if (!url) {
          url = '';
        }

        head = doc.getElementsByTagName('head')[0];

        each(url.split(','), function (url) {
          var link;

          url = Tools._addCacheSuffix(url);

          if (self.files[url]) {
            return;
          }

          self.files[url] = true;
          link = self.create('link', { rel: 'stylesheet', href: url });

          // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug
          // This fix seems to resolve that issue by recalcing the document once a stylesheet finishes loading
          // It's ugly but it seems to work fine.
          if (isIE && doc.documentMode && doc.recalc) {
            link.onload = function () {
              if (doc.recalc) {
                doc.recalc();
              }

              link.onload = null;
            };
          }

          head.appendChild(link);
        });
      },

      /**
       * Adds a class to the specified element or elements.
       *
       * @method addClass
       * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs.
       * @param {String} cls Class name to add to each element.
       * @return {String/Array} String with new class value or array with new class values for all elements.
       * @example
       * // Adds a class to all paragraphs in the active editor
       * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'myclass');
       *
       * // Adds a class to a specific element in the current page
       * tinymce.DOM.addClass('mydiv', 'myclass');
       */
      addClass: function (elm, cls) {
        this.$$(elm).addClass(cls);
      },

      /**
       * Removes a class from the specified element or elements.
       *
       * @method removeClass
       * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs.
       * @param {String} cls Class name to remove from each element.
       * @return {String/Array} String of remaining class name(s), or an array of strings if multiple input elements
       * were passed in.
       * @example
       * // Removes a class from all paragraphs in the active editor
       * tinymce.activeEditor.dom.removeClass(tinymce.activeEditor.dom.select('p'), 'myclass');
       *
       * // Removes a class from a specific element in the current page
       * tinymce.DOM.removeClass('mydiv', 'myclass');
       */
      removeClass: function (elm, cls) {
        this.toggleClass(elm, cls, false);
      },

      /**
       * Returns true if the specified element has the specified class.
       *
       * @method hasClass
       * @param {String/Element} elm HTML element or element id string to check CSS class on.
       * @param {String} cls CSS class to check for.
       * @return {Boolean} true/false if the specified element has the specified class.
       */
      hasClass: function (elm, cls) {
        return this.$$(elm).hasClass(cls);
      },

      /**
       * Toggles the specified class on/off.
       *
       * @method toggleClass
       * @param {Element} elm Element to toggle class on.
       * @param {[type]} cls Class to toggle on/off.
       * @param {[type]} state Optional state to set.
       */
      toggleClass: function (elm, cls, state) {
        this.$$(elm).toggleClass(cls, state).each(function () {
          if (this.className === '') {
            DomQuery(this).attr('class', null);
          }
        });
      },

      /**
       * Shows the specified element(s) by ID by setting the "display" style.
       *
       * @method show
       * @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to show.
       */
      show: function (elm) {
        this.$$(elm).show();
      },

      /**
       * Hides the specified element(s) by ID by setting the "display" style.
       *
       * @method hide
       * @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to hide.
       * @example
       * // Hides an element by id in the document
       * tinymce.DOM.hide('myid');
       */
      hide: function (elm) {
        this.$$(elm).hide();
      },

      /**
       * Returns true/false if the element is hidden or not by checking the "display" style.
       *
       * @method isHidden
       * @param {String/Element} elm Id or element to check display state on.
       * @return {Boolean} true/false if the element is hidden or not.
       */
      isHidden: function (elm) {
        return this.$$(elm).css('display') == 'none';
      },

      /**
       * Returns a unique id. This can be useful when generating elements on the fly.
       * This method will not check if the element already exists.
       *
       * @method uniqueId
       * @param {String} prefix Optional prefix to add in front of all ids - defaults to "mce_".
       * @return {String} Unique id.
       */
      uniqueId: function (prefix) {
        return (!prefix ? 'mce_' : prefix) + (this.counter++);
      },

      /**
       * Sets the specified HTML content inside the element or elements. The HTML will first be processed. This means
       * URLs will get converted, hex color values fixed etc. Check processHTML for details.
       *
       * @method setHTML
       * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set HTML inside of.
       * @param {String} html HTML content to set as inner HTML of the element.
       * @example
       * // Sets the inner HTML of all paragraphs in the active editor
       * tinymce.activeEditor.dom.setHTML(tinymce.activeEditor.dom.select('p'), 'some inner html');
       *
       * // Sets the inner HTML of an element by id in the document
       * tinymce.DOM.setHTML('mydiv', 'some inner html');
       */
      setHTML: function (elm, html) {
        elm = this.$$(elm);

        if (isIE) {
          elm.each(function (i, target) {
            if (target.canHaveHTML === false) {
              return;
            }

            // Remove all child nodes, IE keeps empty text nodes in DOM
            while (target.firstChild) {
              target.removeChild(target.firstChild);
            }

            try {
              // IE will remove comments from the beginning
              // unless you padd the contents with something
              target.innerHTML = '<br>' + html;
              target.removeChild(target.firstChild);
            } catch (ex) {
              // IE sometimes produces an unknown runtime error on innerHTML if it's a div inside a p
              DomQuery('<div></div>').html('<br>' + html).contents().slice(1).appendTo(target);
            }

            return html;
          });
        } else {
          elm.html(html);
        }
      },

      /**
       * Returns the outer HTML of an element.
       *
       * @method getOuterHTML
       * @param {String/Element} elm Element ID or element object to get outer HTML from.
       * @return {String} Outer HTML string.
       * @example
       * tinymce.DOM.getOuterHTML(editorElement);
       * tinymce.activeEditor.getOuterHTML(tinymce.activeEditor.getBody());
       */
      getOuterHTML: function (elm) {
        elm = this.get(elm);

        // Older FF doesn't have outerHTML 3.6 is still used by some orgaizations
        return elm.nodeType == 1 && "outerHTML" in elm ? elm.outerHTML : DomQuery('<div></div>').append(DomQuery(elm).clone()).html();
      },

      /**
       * Sets the specified outer HTML on an element or elements.
       *
       * @method setOuterHTML
       * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set outer HTML on.
       * @param {Object} html HTML code to set as outer value for the element.
       * @example
       * // Sets the outer HTML of all paragraphs in the active editor
       * tinymce.activeEditor.dom.setOuterHTML(tinymce.activeEditor.dom.select('p'), '<div>some html</div>');
       *
       * // Sets the outer HTML of an element by id in the document
       * tinymce.DOM.setOuterHTML('mydiv', '<div>some html</div>');
       */
      setOuterHTML: function (elm, html) {
        var self = this;

        self.$$(elm).each(function () {
          try {
            // Older FF doesn't have outerHTML 3.6 is still used by some organizations
            if ("outerHTML" in this) {
              this.outerHTML = html;
              return;
            }
          } catch (ex) {
            // Ignore
          }

          // OuterHTML for IE it sometimes produces an "unknown runtime error"
          self.remove(DomQuery(this).html(html), true);
        });
      },

      /**
       * Entity decodes a string. This method decodes any HTML entities, such as &aring;.
       *
       * @method decode
       * @param {String} s String to decode entities on.
       * @return {String} Entity decoded string.
       */
      decode: Entities.decode,

      /**
       * Entity encodes a string. This method encodes the most common entities, such as <>"&.
       *
       * @method encode
       * @param {String} text String to encode with entities.
       * @return {String} Entity encoded string.
       */
      encode: Entities.encodeAllRaw,

      /**
       * Inserts an element after the reference element.
       *
       * @method insertAfter
       * @param {Element} node Element to insert after the reference.
       * @param {Element/String/Array} referenceNode Reference element, element id or array of elements to insert after.
       * @return {Element/Array} Element that got added or an array with elements.
       */
      insertAfter: function (node, referenceNode) {
        referenceNode = this.get(referenceNode);

        return this.run(node, function (node) {
          var parent, nextSibling;

          parent = referenceNode.parentNode;
          nextSibling = referenceNode.nextSibling;

          if (nextSibling) {
            parent.insertBefore(node, nextSibling);
          } else {
            parent.appendChild(node);
          }

          return node;
        });
      },

      /**
       * Replaces the specified element or elements with the new element specified. The new element will
       * be cloned if multiple input elements are passed in.
       *
       * @method replace
       * @param {Element} newElm New element to replace old ones with.
       * @param {Element/String/Array} oldElm Element DOM node, element id or array of elements or ids to replace.
       * @param {Boolean} keepChildren Optional keep children state, if set to true child nodes from the old object will be added
       * to new ones.
       */
      replace: function (newElm, oldElm, keepChildren) {
        var self = this;

        return self.run(oldElm, function (oldElm) {
          if (is(oldElm, 'array')) {
            newElm = newElm.cloneNode(true);
          }

          if (keepChildren) {
            each(grep(oldElm.childNodes), function (node) {
              newElm.appendChild(node);
            });
          }

          return oldElm.parentNode.replaceChild(newElm, oldElm);
        });
      },

      /**
       * Renames the specified element and keeps its attributes and children.
       *
       * @method rename
       * @param {Element} elm Element to rename.
       * @param {String} name Name of the new element.
       * @return {Element} New element or the old element if it needed renaming.
       */
      rename: function (elm, name) {
        var self = this, newElm;

        if (elm.nodeName != name.toUpperCase()) {
          // Rename block element
          newElm = self.create(name);

          // Copy attribs to new block
          each(self.getAttribs(elm), function (attrNode) {
            self.setAttrib(newElm, attrNode.nodeName, self.getAttrib(elm, attrNode.nodeName));
          });

          // Replace block
          self.replace(newElm, elm, 1);
        }

        return newElm || elm;
      },

      /**
       * Find the common ancestor of two elements. This is a shorter method than using the DOM Range logic.
       *
       * @method findCommonAncestor
       * @param {Element} a Element to find common ancestor of.
       * @param {Element} b Element to find common ancestor of.
       * @return {Element} Common ancestor element of the two input elements.
       */
      findCommonAncestor: function (a, b) {
        var ps = a, pe;

        while (ps) {
          pe = b;

          while (pe && ps != pe) {
            pe = pe.parentNode;
          }

          if (ps == pe) {
            break;
          }

          ps = ps.parentNode;
        }

        if (!ps && a.ownerDocument) {
          return a.ownerDocument.documentElement;
        }

        return ps;
      },

      /**
       * Parses the specified RGB color value and returns a hex version of that color.
       *
       * @method toHex
       * @param {String} rgbVal RGB string value like rgb(1,2,3)
       * @return {String} Hex version of that RGB value like #FF00FF.
       */
      toHex: function (rgbVal) {
        return this.styles.toHex(Tools.trim(rgbVal));
      },

      /**
       * Executes the specified function on the element by id or dom element node or array of elements/id.
       *
       * @method run
       * @param {String/Element/Array} elm ID or DOM element object or array with ids or elements.
       * @param {function} func Function to execute for each item.
       * @param {Object} scope Optional scope to execute the function in.
       * @return {Object/Array} Single object, or an array of objects if multiple input elements were passed in.
       */
      run: function (elm, func, scope) {
        var self = this, result;

        if (typeof elm === 'string') {
          elm = self.get(elm);
        }

        if (!elm) {
          return false;
        }

        scope = scope || this;
        if (!elm.nodeType && (elm.length || elm.length === 0)) {
          result = [];

          each(elm, function (elm, i) {
            if (elm) {
              if (typeof elm == 'string') {
                elm = self.get(elm);
              }

              result.push(func.call(scope, elm, i));
            }
          });

          return result;
        }

        return func.call(scope, elm);
      },

      /**
       * Returns a NodeList with attributes for the element.
       *
       * @method getAttribs
       * @param {HTMLElement/string} elm Element node or string id to get attributes from.
       * @return {NodeList} NodeList with attributes.
       */
      getAttribs: function (elm) {
        var attrs;

        elm = this.get(elm);

        if (!elm) {
          return [];
        }

        if (isIE) {
          attrs = [];

          // Object will throw exception in IE
          if (elm.nodeName == 'OBJECT') {
            return elm.attributes;
          }

          // IE doesn't keep the selected attribute if you clone option elements
          if (elm.nodeName === 'OPTION' && this.getAttrib(elm, 'selected')) {
            attrs.push({ specified: 1, nodeName: 'selected' });
          }

          // It's crazy that this is faster in IE but it's because it returns all attributes all the time
          var attrRegExp = /<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi;
          elm.cloneNode(false).outerHTML.replace(attrRegExp, '').replace(/[\w:\-]+/gi, function (a) {
            attrs.push({ specified: 1, nodeName: a });
          });

          return attrs;
        }

        return elm.attributes;
      },

      /**
       * Returns true/false if the specified node is to be considered empty or not.
       *
       * @example
       * tinymce.DOM.isEmpty(node, {img: true});
       * @method isEmpty
       * @param {Object} elements Optional name/value object with elements that are automatically treated as non-empty elements.
       * @return {Boolean} true/false if the node is empty or not.
       */
      isEmpty: function (node, elements) {
        var self = this, i, attributes, type, whitespace, walker, name, brCount = 0;

        node = node.firstChild;
        if (node) {
          walker = new TreeWalker(node, node.parentNode);
          elements = elements || (self.schema ? self.schema.getNonEmptyElements() : null);
          whitespace = self.schema ? self.schema.getWhiteSpaceElements() : {};

          do {
            type = node.nodeType;

            if (type === 1) {
              // Ignore bogus elements
              var bogusVal = node.getAttribute('data-mce-bogus');
              if (bogusVal) {
                node = walker.next(bogusVal === 'all');
                continue;
              }

              // Keep empty elements like <img />
              name = node.nodeName.toLowerCase();
              if (elements && elements[name]) {
                // Ignore single BR elements in blocks like <p><br /></p> or <p><span><br /></span></p>
                if (name === 'br') {
                  brCount++;
                  node = walker.next();
                  continue;
                }

                return false;
              }

              // Keep elements with data-bookmark attributes or name attribute like <a name="1"></a>
              attributes = self.getAttribs(node);
              i = attributes.length;
              while (i--) {
                name = attributes[i].nodeName;
                if (name === "name" || name === 'data-mce-bookmark') {
                  return false;
                }
              }
            }

            // Keep comment nodes
            if (type == 8) {
              return false;
            }

            // Keep non whitespace text nodes
            if (type === 3 && !whiteSpaceRegExp.test(node.nodeValue)) {
              return false;
            }

            // Keep whitespace preserve elements
            if (type === 3 && node.parentNode && whitespace[node.parentNode.nodeName] && whiteSpaceRegExp.test(node.nodeValue)) {
              return false;
            }

            node = walker.next();
          } while (node);
        }

        return brCount <= 1;
      },

      /**
       * Creates a new DOM Range object. This will use the native DOM Range API if it's
       * available. If it's not, it will fall back to the custom TinyMCE implementation.
       *
       * @method createRng
       * @return {DOMRange} DOM Range object.
       * @example
       * var rng = tinymce.DOM.createRng();
       * alert(rng.startContainer + "," + rng.startOffset);
       */
      createRng: function () {
        return this.doc.createRange();
      },

      /**
       * Returns the index of the specified node within its parent.
       *
       * @method nodeIndex
       * @param {Node} node Node to look for.
       * @param {boolean} normalized Optional true/false state if the index is what it would be after a normalization.
       * @return {Number} Index of the specified node.
       */
      nodeIndex: nodeIndex,

      /**
       * Splits an element into two new elements and places the specified split
       * element or elements between the new ones. For example splitting the paragraph at the bold element in
       * this example <p>abc<b>abc</b>123</p> would produce <p>abc</p><b>abc</b><p>123</p>.
       *
       * @method split
       * @param {Element} parentElm Parent element to split.
       * @param {Element} splitElm Element to split at.
       * @param {Element} replacementElm Optional replacement element to replace the split element with.
       * @return {Element} Returns the split element or the replacement element if that is specified.
       */
      split: function (parentElm, splitElm, replacementElm) {
        var self = this, r = self.createRng(), bef, aft, pa;

        // W3C valid browsers tend to leave empty nodes to the left/right side of the contents - this makes sense
        // but we don't want that in our code since it serves no purpose for the end user
        // For example splitting this html at the bold element:
        //   <p>text 1<span><b>CHOP</b></span>text 2</p>
        // would produce:
        //   <p>text 1<span></span></p><b>CHOP</b><p><span></span>text 2</p>
        // this function will then trim off empty edges and produce:
        //   <p>text 1</p><b>CHOP</b><p>text 2</p>
        var trimNode = function (node) {
          var i, children = node.childNodes, type = node.nodeType;

          var surroundedBySpans = function (node) {
            var previousIsSpan = node.previousSibling && node.previousSibling.nodeName == 'SPAN';
            var nextIsSpan = node.nextSibling && node.nextSibling.nodeName == 'SPAN';
            return previousIsSpan && nextIsSpan;
          };

          if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark') {
            return;
          }

          for (i = children.length - 1; i >= 0; i--) {
            trimNode(children[i]);
          }

          if (type != 9) {
            // Keep non whitespace text nodes
            if (type == 3 && node.nodeValue.length > 0) {
              // If parent element isn't a block or there isn't any useful contents for example "<p>   </p>"
              // Also keep text nodes with only spaces if surrounded by spans.
              // eg. "<p><span>a</span> <span>b</span></p>" should keep space between a and b
              var trimmedLength = trim(node.nodeValue).length;
              if (!self.isBlock(node.parentNode) || trimmedLength > 0 || trimmedLength === 0 && surroundedBySpans(node)) {
                return;
              }
            } else if (type == 1) {
              // If the only child is a bookmark then move it up
              children = node.childNodes;

              // TODO fix this complex if
              if (children.length == 1 && children[0] && children[0].nodeType == 1 &&
                children[0].getAttribute('data-mce-type') == 'bookmark') {
                node.parentNode.insertBefore(children[0], node);
              }

              // Keep non empty elements or img, hr etc
              if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) {
                return;
              }
            }

            self.remove(node);
          }

          return node;
        };

        if (parentElm && splitElm) {
          // Get before chunk
          r.setStart(parentElm.parentNode, self.nodeIndex(parentElm));
          r.setEnd(splitElm.parentNode, self.nodeIndex(splitElm));
          bef = r.extractContents();

          // Get after chunk
          r = self.createRng();
          r.setStart(splitElm.parentNode, self.nodeIndex(splitElm) + 1);
          r.setEnd(parentElm.parentNode, self.nodeIndex(parentElm) + 1);
          aft = r.extractContents();

          // Insert before chunk
          pa = parentElm.parentNode;
          pa.insertBefore(trimNode(bef), parentElm);

          // Insert middle chunk
          if (replacementElm) {
            pa.insertBefore(replacementElm, parentElm);
            //pa.replaceChild(replacementElm, splitElm);
          } else {
            pa.insertBefore(splitElm, parentElm);
          }

          // Insert after chunk
          pa.insertBefore(trimNode(aft), parentElm);
          self.remove(parentElm);

          return replacementElm || splitElm;
        }
      },

      /**
       * Adds an event handler to the specified object.
       *
       * @method bind
       * @param {Element/Document/Window/Array} target Target element to bind events to.
       * handler to or an array of elements/ids/documents.
       * @param {String} name Name of event handler to add, for example: click.
       * @param {function} func Function to execute when the event occurs.
       * @param {Object} scope Optional scope to execute the function in.
       * @return {function} Function callback handler the same as the one passed in.
       */
      bind: function (target, name, func, scope) {
        var self = this;

        if (Tools.isArray(target)) {
          var i = target.length;

          while (i--) {
            target[i] = self.bind(target[i], name, func, scope);
          }

          return target;
        }

        // Collect all window/document events bound by editor instance
        if (self.settings.collect && (target === self.doc || target === self.win)) {
          self.boundEvents.push([target, name, func, scope]);
        }

        return self.events.bind(target, name, func, scope || self);
      },

      /**
       * Removes the specified event handler by name and function from an element or collection of elements.
       *
       * @method unbind
       * @param {Element/Document/Window/Array} target Target element to unbind events on.
       * @param {String} name Event handler name, for example: "click"
       * @param {function} func Function to remove.
       * @return {bool/Array} Bool state of true if the handler was removed, or an array of states if multiple input elements
       * were passed in.
       */
      unbind: function (target, name, func) {
        var self = this, i;

        if (Tools.isArray(target)) {
          i = target.length;

          while (i--) {
            target[i] = self.unbind(target[i], name, func);
          }

          return target;
        }

        // Remove any bound events matching the input
        if (self.boundEvents && (target === self.doc || target === self.win)) {
          i = self.boundEvents.length;

          while (i--) {
            var item = self.boundEvents[i];

            if (target == item[0] && (!name || name == item[1]) && (!func || func == item[2])) {
              this.events.unbind(item[0], item[1], item[2]);
            }
          }
        }

        return this.events.unbind(target, name, func);
      },

      /**
       * Fires the specified event name with object on target.
       *
       * @method fire
       * @param {Node/Document/Window} target Target element or object to fire event on.
       * @param {String} name Name of the event to fire.
       * @param {Object} evt Event object to send.
       * @return {Event} Event object.
       */
      fire: function (target, name, evt) {
        return this.events.fire(target, name, evt);
      },

      // Returns the content editable state of a node
      getContentEditable: function (node) {
        var contentEditable;

        // Check type
        if (!node || node.nodeType != 1) {
          return null;
        }

        // Check for fake content editable
        contentEditable = node.getAttribute("data-mce-contenteditable");
        if (contentEditable && contentEditable !== "inherit") {
          return contentEditable;
        }

        // Check for real content editable
        return node.contentEditable !== "inherit" ? node.contentEditable : null;
      },

      getContentEditableParent: function (node) {
        var root = this.getRoot(), state = null;

        for (; node && node !== root; node = node.parentNode) {
          state = this.getContentEditable(node);

          if (state !== null) {
            break;
          }
        }

        return state;
      },

      /**
       * Destroys all internal references to the DOM to solve IE leak issues.
       *
       * @method destroy
       */
      destroy: function () {
        var self = this;

        // Unbind all events bound to window/document by editor instance
        if (self.boundEvents) {
          var i = self.boundEvents.length;

          while (i--) {
            var item = self.boundEvents[i];
            this.events.unbind(item[0], item[1], item[2]);
          }

          self.boundEvents = null;
        }

        // Restore sizzle document to window.document
        // Since the current document might be removed producing "Permission denied" on IE see #6325
        if (Sizzle.setDocument) {
          Sizzle.setDocument();
        }

        self.win = self.doc = self.root = self.events = self.frag = null;
      },

      isChildOf: function (node, parent) {
        while (node) {
          if (parent === node) {
            return true;
          }

          node = node.parentNode;
        }

        return false;
      },

      // #ifdef debug

      dumpRng: function (r) {
        return (
          'startContainer: ' + r.startContainer.nodeName +
          ', startOffset: ' + r.startOffset +
          ', endContainer: ' + r.endContainer.nodeName +
          ', endOffset: ' + r.endOffset
        );
      },

      // #endif

      _findSib: function (node, selector, name) {
        var self = this, func = selector;

        if (node) {
          // If expression make a function of it using is
          if (typeof func == 'string') {
            func = function (node) {
              return self.is(node, selector);
            };
          }

          // Loop all siblings
          for (node = node[name]; node; node = node[name]) {
            if (func(node)) {
              return node;
            }
          }
        }

        return null;
      }
    };

    /**
     * Instance of DOMUtils for the current document.
     *
     * @static
     * @property DOM
     * @type tinymce.dom.DOMUtils
     * @example
     * // Example of how to add a class to some element by id
     * tinymce.DOM.addClass('someid', 'someclass');
     */
    DOMUtils.DOM = new DOMUtils(document);
    DOMUtils.nodeIndex = nodeIndex;

    return DOMUtils;
  }
);

/**
 * ScriptLoader.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
 */

/*globals console*/

/**
 * This class handles asynchronous/synchronous loading of JavaScript files it will execute callbacks
 * when various items gets loaded. This class is useful to load external JavaScript files.
 *
 * @class tinymce.dom.ScriptLoader
 * @example
 * // Load a script from a specific URL using the global script loader
 * tinymce.ScriptLoader.load('somescript.js');
 *
 * // Load a script using a unique instance of the script loader
 * var scriptLoader = new tinymce.dom.ScriptLoader();
 *
 * scriptLoader.load('somescript.js');
 *
 * // Load multiple scripts
 * var scriptLoader = new tinymce.dom.ScriptLoader();
 *
 * scriptLoader.add('somescript1.js');
 * scriptLoader.add('somescript2.js');
 * scriptLoader.add('somescript3.js');
 *
 * scriptLoader.loadQueue(function() {
 *    alert('All scripts are now loaded.');
 * });
 */
define(
  'tinymce.core.dom.ScriptLoader',
  [
    'global!document',
    'tinymce.core.dom.DOMUtils',
    'tinymce.core.util.Tools'
  ],
  function (document, DOMUtils, Tools) {
    var DOM = DOMUtils.DOM;
    var each = Tools.each, grep = Tools.grep;

    var isFunction = function (f) {
      return typeof f === 'function';
    };

    var ScriptLoader = function () {
      var QUEUED = 0,
        LOADING = 1,
        LOADED = 2,
        FAILED = 3,
        states = {},
        queue = [],
        scriptLoadedCallbacks = {},
        queueLoadedCallbacks = [],
        loading = 0,
        undef;

      /**
       * Loads a specific script directly without adding it to the load queue.
       *
       * @method load
       * @param {String} url Absolute URL to script to add.
       * @param {function} callback Optional success callback function when the script loaded successfully.
       * @param {function} callback Optional failure callback function when the script failed to load.
       */
      var loadScript = function (url, success, failure) {
        var dom = DOM, elm, id;

        // Execute callback when script is loaded
        var done = function () {
          dom.remove(id);

          if (elm) {
            elm.onreadystatechange = elm.onload = elm = null;
          }

          success();
        };

        var error = function () {
          /*eslint no-console:0 */

          // We can't mark it as done if there is a load error since
          // A) We don't want to produce 404 errors on the server and
          // B) the onerror event won't fire on all browsers.
          // done();

          if (isFunction(failure)) {
            failure();
          } else {
            // Report the error so it's easier for people to spot loading errors
            if (typeof console !== "undefined" && console.log) {
              console.log("Failed to load script: " + url);
            }
          }
        };

        id = dom.uniqueId();

        // Create new script element
        elm = document.createElement('script');
        elm.id = id;
        elm.type = 'text/javascript';
        elm.src = Tools._addCacheSuffix(url);

        // Seems that onreadystatechange works better on IE 10 onload seems to fire incorrectly
        if ("onreadystatechange" in elm) {
          elm.onreadystatechange = function () {
            if (/loaded|complete/.test(elm.readyState)) {
              done();
            }
          };
        } else {
          elm.onload = done;
        }

        // Add onerror event will get fired on some browsers but not all of them
        elm.onerror = error;

        // Add script to document
        (document.getElementsByTagName('head')[0] || document.body).appendChild(elm);
      };

      /**
       * Returns true/false if a script has been loaded or not.
       *
       * @method isDone
       * @param {String} url URL to check for.
       * @return {Boolean} true/false if the URL is loaded.
       */
      this.isDone = function (url) {
        return states[url] == LOADED;
      };

      /**
       * Marks a specific script to be loaded. This can be useful if a script got loaded outside
       * the script loader or to skip it from loading some script.
       *
       * @method markDone
       * @param {string} url Absolute URL to the script to mark as loaded.
       */
      this.markDone = function (url) {
        states[url] = LOADED;
      };

      /**
       * Adds a specific script to the load queue of the script loader.
       *
       * @method add
       * @param {String} url Absolute URL to script to add.
       * @param {function} success Optional success callback function to execute when the script loades successfully.
       * @param {Object} scope Optional scope to execute callback in.
       * @param {function} failure Optional failure callback function to execute when the script failed to load.
       */
      this.add = this.load = function (url, success, scope, failure) {
        var state = states[url];

        // Add url to load queue
        if (state == undef) {
          queue.push(url);
          states[url] = QUEUED;
        }

        if (success) {
          // Store away callback for later execution
          if (!scriptLoadedCallbacks[url]) {
            scriptLoadedCallbacks[url] = [];
          }

          scriptLoadedCallbacks[url].push({
            success: success,
            failure: failure,
            scope: scope || this
          });
        }
      };

      this.remove = function (url) {
        delete states[url];
        delete scriptLoadedCallbacks[url];
      };

      /**
       * Starts the loading of the queue.
       *
       * @method loadQueue
       * @param {function} success Optional callback to execute when all queued items are loaded.
       * @param {function} failure Optional callback to execute when queued items failed to load.
       * @param {Object} scope Optional scope to execute the callback in.
       */
      this.loadQueue = function (success, scope, failure) {
        this.loadScripts(queue, success, scope, failure);
      };

      /**
       * Loads the specified queue of files and executes the callback ones they are loaded.
       * This method is generally not used outside this class but it might be useful in some scenarios.
       *
       * @method loadScripts
       * @param {Array} scripts Array of queue items to load.
       * @param {function} callback Optional callback to execute when scripts is loaded successfully.
       * @param {Object} scope Optional scope to execute callback in.
       * @param {function} callback Optional callback to execute if scripts failed to load.
       */
      this.loadScripts = function (scripts, success, scope, failure) {
        var loadScripts, failures = [];

        var execCallbacks = function (name, url) {
          // Execute URL callback functions
          each(scriptLoadedCallbacks[url], function (callback) {
            if (isFunction(callback[name])) {
              callback[name].call(callback.scope);
            }
          });

          scriptLoadedCallbacks[url] = undef;
        };

        queueLoadedCallbacks.push({
          success: success,
          failure: failure,
          scope: scope || this
        });

        loadScripts = function () {
          var loadingScripts = grep(scripts);

          // Current scripts has been handled
          scripts.length = 0;

          // Load scripts that needs to be loaded
          each(loadingScripts, function (url) {
            // Script is already loaded then execute script callbacks directly
            if (states[url] === LOADED) {
              execCallbacks('success', url);
              return;
            }

            if (states[url] === FAILED) {
              execCallbacks('failure', url);
              return;
            }

            // Is script not loading then start loading it
            if (states[url] !== LOADING) {
              states[url] = LOADING;
              loading++;

              loadScript(url, function () {
                states[url] = LOADED;
                loading--;

                execCallbacks('success', url);

                // Load more scripts if they where added by the recently loaded script
                loadScripts();
              }, function () {
                states[url] = FAILED;
                loading--;

                failures.push(url);
                execCallbacks('failure', url);

                // Load more scripts if they where added by the recently loaded script
                loadScripts();
              });
            }
          });

          // No scripts are currently loading then execute all pending queue loaded callbacks
          if (!loading) {
            // We need to clone the notifications and empty the pending callbacks so that callbacks can load more resources
            var notifyCallbacks = queueLoadedCallbacks.slice(0);
            queueLoadedCallbacks.length = 0;

            each(notifyCallbacks, function (callback) {
              if (failures.length === 0) {
                if (isFunction(callback.success)) {
                  callback.success.call(callback.scope);
                }
              } else {
                if (isFunction(callback.failure)) {
                  callback.failure.call(callback.scope, failures);
                }
              }
            });
          }
        };

        loadScripts();
      };
    };

    ScriptLoader.ScriptLoader = new ScriptLoader();

    return ScriptLoader;
  }
);

/**
 * AddOnManager.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
 */

/**
 * This class handles the loading of themes/plugins or other add-ons and their language packs.
 *
 * @class tinymce.AddOnManager
 */
define(
  'tinymce.core.AddOnManager',
  [
    'ephox.katamari.api.Arr',
    'tinymce.core.dom.ScriptLoader',
    'tinymce.core.util.Tools'
  ],
  function (Arr, ScriptLoader, Tools) {
    var each = Tools.each;

    var AddOnManager = function () {
      var self = this;

      self.items = [];
      self.urls = {};
      self.lookup = {};
      self._listeners = [];
    };

    AddOnManager.prototype = {
      /**
       * Returns the specified add on by the short name.
       *
       * @method get
       * @param {String} name Add-on to look for.
       * @return {tinymce.Theme/tinymce.Plugin} Theme or plugin add-on instance or undefined.
       */
      get: function (name) {
        if (this.lookup[name]) {
          return this.lookup[name].instance;
        }

        return undefined;
      },

      dependencies: function (name) {
        var result;

        if (this.lookup[name]) {
          result = this.lookup[name].dependencies;
        }

        return result || [];
      },

      /**
       * Loads a language pack for the specified add-on.
       *
       * @method requireLangPack
       * @param {String} name Short name of the add-on.
       * @param {String} languages Optional comma or space separated list of languages to check if it matches the name.
       */
      requireLangPack: function (name, languages) {
        var language = AddOnManager.language;

        if (language && AddOnManager.languageLoad !== false) {
          if (languages) {
            languages = ',' + languages + ',';

            // Load short form sv.js or long form sv_SE.js
            if (languages.indexOf(',' + language.substr(0, 2) + ',') != -1) {
              language = language.substr(0, 2);
            } else if (languages.indexOf(',' + language + ',') == -1) {
              return;
            }
          }

          ScriptLoader.ScriptLoader.add(this.urls[name] + '/langs/' + language + '.js');
        }
      },

      /**
       * Adds a instance of the add-on by it's short name.
       *
       * @method add
       * @param {String} id Short name/id for the add-on.
       * @param {tinymce.Theme/tinymce.Plugin} addOn Theme or plugin to add.
       * @return {tinymce.Theme/tinymce.Plugin} The same theme or plugin instance that got passed in.
       * @example
       * // Create a simple plugin
       * tinymce.create('tinymce.plugins.TestPlugin', {
       *   TestPlugin: function(ed, url) {
       *   ed.on('click', function(e) {
       *      ed.windowManager.alert('Hello World!');
       *   });
       *   }
       * });
       *
       * // Register plugin using the add method
       * tinymce.PluginManager.add('test', tinymce.plugins.TestPlugin);
       *
       * // Initialize TinyMCE
       * tinymce.init({
       *  ...
       *  plugins: '-test' // Init the plugin but don't try to load it
       * });
       */
      add: function (id, addOn, dependencies) {
        this.items.push(addOn);
        this.lookup[id] = { instance: addOn, dependencies: dependencies };
        var result = Arr.partition(this._listeners, function (listener) {
          return listener.name === id;
        });

        this._listeners = result.fail;

        each(result.pass, function (listener) {
          listener.callback();
        });

        return addOn;
      },

      remove: function (name) {
        delete this.urls[name];
        delete this.lookup[name];
      },

      createUrl: function (baseUrl, dep) {
        if (typeof dep === "object") {
          return dep;
        }

        return { prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix };
      },

      /**
       * Add a set of components that will make up the add-on. Using the url of the add-on name as the base url.
       * This should be used in development mode.  A new compressor/javascript munger process will ensure that the
       * components are put together into the plugin.js file and compressed correctly.
       *
       * @method addComponents
       * @param {String} pluginName name of the plugin to load scripts from (will be used to get the base url for the plugins).
       * @param {Array} scripts Array containing the names of the scripts to load.
       */
      addComponents: function (pluginName, scripts) {
        var pluginUrl = this.urls[pluginName];

        each(scripts, function (script) {
          ScriptLoader.ScriptLoader.add(pluginUrl + "/" + script);
        });
      },

      /**
       * Loads an add-on from a specific url.
       *
       * @method load
       * @param {String} name Short name of the add-on that gets loaded.
       * @param {String} addOnUrl URL to the add-on that will get loaded.
       * @param {function} success Optional success callback to execute when an add-on is loaded.
       * @param {Object} scope Optional scope to execute the callback in.
       * @param {function} failure Optional failure callback to execute when an add-on failed to load.
       * @example
       * // Loads a plugin from an external URL
       * tinymce.PluginManager.load('myplugin', '/some/dir/someplugin/plugin.js');
       *
       * // Initialize TinyMCE
       * tinymce.init({
       *  ...
       *  plugins: '-myplugin' // Don't try to load it again
       * });
       */
      load: function (name, addOnUrl, success, scope, failure) {
        var self = this, url = addOnUrl;

        var loadDependencies = function () {
          var dependencies = self.dependencies(name);

          each(dependencies, function (dep) {
            var newUrl = self.createUrl(addOnUrl, dep);

            self.load(newUrl.resource, newUrl, undefined, undefined);
          });

          if (success) {
            if (scope) {
              success.call(scope);
            } else {
              success.call(ScriptLoader);
            }
          }
        };

        if (self.urls[name]) {
          return;
        }

        if (typeof addOnUrl === "object") {
          url = addOnUrl.prefix + addOnUrl.resource + addOnUrl.suffix;
        }

        if (url.indexOf('/') !== 0 && url.indexOf('://') == -1) {
          url = AddOnManager.baseURL + '/' + url;
        }

        self.urls[name] = url.substring(0, url.lastIndexOf('/'));

        if (self.lookup[name]) {
          loadDependencies();
        } else {
          ScriptLoader.ScriptLoader.add(url, loadDependencies, scope, failure);
        }
      },

      waitFor: function (name, callback) {
        if (this.lookup.hasOwnProperty(name)) {
          callback();
        } else {
          this._listeners.push({ name: name, callback: callback });
        }
      }
    };

    AddOnManager.PluginManager = new AddOnManager();
    AddOnManager.ThemeManager = new AddOnManager();

    return AddOnManager;
  }
);

/**
 * TinyMCE theme class.
 *
 * @class tinymce.Theme
 */

/**
 * This method is responsible for rendering/generating the overall user interface with toolbars, buttons, iframe containers etc.
 *
 * @method renderUI
 * @param {Object} obj Object parameter containing the targetNode DOM node that will be replaced visually with an editor instance.
 * @return {Object} an object with items like iframeContainer, editorContainer, sizeContainer, deltaWidth, deltaHeight.
 */

/**
 * Plugin base class, this is a pseudo class that describes how a plugin is to be created for TinyMCE. The methods below are all optional.
 *
 * @class tinymce.Plugin
 * @example
 * tinymce.PluginManager.add('example', function(editor, url) {
 *     // Add a button that opens a window
 *     editor.addButton('example', {
 *         text: 'My button',
 *         icon: false,
 *         onclick: function() {
 *             // Open window
 *             editor.windowManager.open({
 *                 title: 'Example plugin',
 *                 body: [
 *                     {type: 'textbox', name: 'title', label: 'Title'}
 *                 ],
 *                 onsubmit: function(e) {
 *                     // Insert content when the window form is submitted
 *                     editor.insertContent('Title: ' + e.data.title);
 *                 }
 *             });
 *         }
 *     });
 *
 *     // Adds a menu item to the tools menu
 *     editor.addMenuItem('example', {
 *         text: 'Example plugin',
 *         context: 'tools',
 *         onclick: function() {
 *             // Open window with a specific url
 *             editor.windowManager.open({
 *                 title: 'TinyMCE site',
 *                 url: 'http://www.tinymce.com',
 *                 width: 800,
 *                 height: 600,
 *                 buttons: [{
 *                     text: 'Close',
 *                     onclick: 'close'
 *                 }]
 *             });
 *         }
 *     });
 * });
 */

define(
  'ephox.katamari.api.Cell',

  [
  ],

  function () {
    var Cell = function (initial) {
      var value = initial;

      var get = function () {
        return value;
      };

      var set = function (v) {
        value = v;
      };

      var clone = function () {
        return Cell(get());
      };

      return {
        get: get,
        set: set,
        clone: clone
      };
    };

    return Cell;
  }
);

/**
 * NodeType.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
 */

/**
 * Contains various node validation functions.
 *
 * @private
 * @class tinymce.dom.NodeType
 */
define(
  'tinymce.core.dom.NodeType',
  [
  ],
  function () {
    var isNodeType = function (type) {
      return function (node) {
        return !!node && node.nodeType == type;
      };
    };

    var isElement = isNodeType(1);

    var matchNodeNames = function (names) {
      names = names.toLowerCase().split(' ');

      return function (node) {
        var i, name;

        if (node && node.nodeType) {
          name = node.nodeName.toLowerCase();

          for (i = 0; i < names.length; i++) {
            if (name === names[i]) {
              return true;
            }
          }
        }

        return false;
      };
    };

    var matchStyleValues = function (name, values) {
      values = values.toLowerCase().split(' ');

      return function (node) {
        var i, cssValue;

        if (isElement(node)) {
          for (i = 0; i < values.length; i++) {
            cssValue = node.ownerDocument.defaultView.getComputedStyle(node, null).getPropertyValue(name);
            if (cssValue === values[i]) {
              return true;
            }
          }
        }

        return false;
      };
    };

    var hasPropValue = function (propName, propValue) {
      return function (node) {
        return isElement(node) && node[propName] === propValue;
      };
    };

    var hasAttribute = function (attrName, attrValue) {
      return function (node) {
        return isElement(node) && node.hasAttribute(attrName);
      };
    };

    var hasAttributeValue = function (attrName, attrValue) {
      return function (node) {
        return isElement(node) && node.getAttribute(attrName) === attrValue;
      };
    };

    var isBogus = function (node) {
      return isElement(node) && node.hasAttribute('data-mce-bogus');
    };

    var hasContentEditableState = function (value) {
      return function (node) {
        if (isElement(node)) {
          if (node.contentEditable === value) {
            return true;
          }

          if (node.getAttribute('data-mce-contenteditable') === value) {
            return true;
          }
        }

        return false;
      };
    };

    return {
      isText: isNodeType(3),
      isElement: isElement,
      isComment: isNodeType(8),
      isBr: matchNodeNames('br'),
      isContentEditableTrue: hasContentEditableState('true'),
      isContentEditableFalse: hasContentEditableState('false'),
      matchNodeNames: matchNodeNames,
      hasPropValue: hasPropValue,
      hasAttribute: hasAttribute,
      hasAttributeValue: hasAttributeValue,
      matchStyleValues: matchStyleValues,
      isBogus: isBogus
    };
  }
);
/**
 * Fun.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
 */

/**
 * Functional utility class.
 *
 * @private
 * @class tinymce.util.Fun
 */
define(
  'tinymce.core.util.Fun',
  [
  ],
  function () {
    var slice = [].slice;

    var constant = function (value) {
      return function () {
        return value;
      };
    };

    var negate = function (predicate) {
      return function (x) {
        return !predicate(x);
      };
    };

    var compose = function (f, g) {
      return function (x) {
        return f(g(x));
      };
    };

    var or = function () {
      var args = slice.call(arguments);

      return function (x) {
        for (var i = 0; i < args.length; i++) {
          if (args[i](x)) {
            return true;
          }
        }

        return false;
      };
    };

    var and = function () {
      var args = slice.call(arguments);

      return function (x) {
        for (var i = 0; i < args.length; i++) {
          if (!args[i](x)) {
            return false;
          }
        }

        return true;
      };
    };

    var curry = function (fn) {
      var args = slice.call(arguments);

      if (args.length - 1 >= fn.length) {
        return fn.apply(this, args.slice(1));
      }

      return function () {
        var tempArgs = args.concat([].slice.call(arguments));
        return curry.apply(this, tempArgs);
      };
    };

    var noop = function () {
    };

    return {
      constant: constant,
      negate: negate,
      and: and,
      or: or,
      curry: curry,
      compose: compose,
      noop: noop
    };
  }
);
/**
 * Zwsp.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
 */

/**
 * Utility functions for working with zero width space
 * characters used as character containers etc.
 *
 * @private
 * @class tinymce.text.Zwsp
 * @example
 * var isZwsp = Zwsp.isZwsp('\uFEFF');
 * var abc = Zwsp.trim('a\uFEFFc');
 */
define(
  'tinymce.core.text.Zwsp',
  [
  ],
  function () {
    // This is technically not a ZWSP but a ZWNBSP or a BYTE ORDER MARK it used to be a ZWSP
    var ZWSP = '\uFEFF';

    var isZwsp = function (chr) {
      return chr === ZWSP;
    };

    var trim = function (text) {
      return text.replace(new RegExp(ZWSP, 'g'), '');
    };

    return {
      isZwsp: isZwsp,
      ZWSP: ZWSP,
      trim: trim
    };
  }
);
/**
 * CaretContainer.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
 */

/**
 * This module handles caret containers. A caret container is a node that
 * holds the caret for positional purposes.
 *
 * @private
 * @class tinymce.caret.CaretContainer
 */
define(
  'tinymce.core.caret.CaretContainer',
  [
    'global!document',
    'tinymce.core.dom.NodeType',
    'tinymce.core.text.Zwsp'
  ],
  function (document, NodeType, Zwsp) {
    var isElement = NodeType.isElement,
      isText = NodeType.isText;

    var isCaretContainerBlock = function (node) {
      if (isText(node)) {
        node = node.parentNode;
      }

      return isElement(node) && node.hasAttribute('data-mce-caret');
    };

    var isCaretContainerInline = function (node) {
      return isText(node) && Zwsp.isZwsp(node.data);
    };

    var isCaretContainer = function (node) {
      return isCaretContainerBlock(node) || isCaretContainerInline(node);
    };

    var hasContent = function (node) {
      return node.firstChild !== node.lastChild || !NodeType.isBr(node.firstChild);
    };

    var insertInline = function (node, before) {
      var doc, sibling, textNode, parentNode;

      doc = node.ownerDocument;
      textNode = doc.createTextNode(Zwsp.ZWSP);
      parentNode = node.parentNode;

      if (!before) {
        sibling = node.nextSibling;
        if (isText(sibling)) {
          if (isCaretContainer(sibling)) {
            return sibling;
          }

          if (startsWithCaretContainer(sibling)) {
            sibling.splitText(1);
            return sibling;
          }
        }

        if (node.nextSibling) {
          parentNode.insertBefore(textNode, node.nextSibling);
        } else {
          parentNode.appendChild(textNode);
        }
      } else {
        sibling = node.previousSibling;
        if (isText(sibling)) {
          if (isCaretContainer(sibling)) {
            return sibling;
          }

          if (endsWithCaretContainer(sibling)) {
            return sibling.splitText(sibling.data.length - 1);
          }
        }

        parentNode.insertBefore(textNode, node);
      }

      return textNode;
    };

    var prependInline = function (node) {
      if (NodeType.isText(node)) {
        var data = node.data;
        if (data.length > 0 && data.charAt(0) !== Zwsp.ZWSP) {
          node.insertData(0, Zwsp.ZWSP);
        }
        return node;
      } else {
        return null;
      }
    };

    var appendInline = function (node) {
      if (NodeType.isText(node)) {
        var data = node.data;
        if (data.length > 0 && data.charAt(data.length - 1) !== Zwsp.ZWSP) {
          node.insertData(data.length, Zwsp.ZWSP);
        }
        return node;
      } else {
        return null;
      }
    };

    var isBeforeInline = function (pos) {
      return pos && NodeType.isText(pos.container()) && pos.container().data.charAt(pos.offset()) === Zwsp.ZWSP;
    };

    var isAfterInline = function (pos) {
      return pos && NodeType.isText(pos.container()) && pos.container().data.charAt(pos.offset() - 1) === Zwsp.ZWSP;
    };

    var createBogusBr = function () {
      var br = document.createElement('br');
      br.setAttribute('data-mce-bogus', '1');
      return br;
    };

    var insertBlock = function (blockName, node, before) {
      var doc, blockNode, parentNode;

      doc = node.ownerDocument;
      blockNode = doc.createElement(blockName);
      blockNode.setAttribute('data-mce-caret', before ? 'before' : 'after');
      blockNode.setAttribute('data-mce-bogus', 'all');
      blockNode.appendChild(createBogusBr());
      parentNode = node.parentNode;

      if (!before) {
        if (node.nextSibling) {
          parentNode.insertBefore(blockNode, node.nextSibling);
        } else {
          parentNode.appendChild(blockNode);
        }
      } else {
        parentNode.insertBefore(blockNode, node);
      }

      return blockNode;
    };

    var startsWithCaretContainer = function (node) {
      return isText(node) && node.data[0] == Zwsp.ZWSP;
    };

    var endsWithCaretContainer = function (node) {
      return isText(node) && node.data[node.data.length - 1] == Zwsp.ZWSP;
    };

    var trimBogusBr = function (elm) {
      var brs = elm.getElementsByTagName('br');
      var lastBr = brs[brs.length - 1];
      if (NodeType.isBogus(lastBr)) {
        lastBr.parentNode.removeChild(lastBr);
      }
    };

    var showCaretContainerBlock = function (caretContainer) {
      if (caretContainer && caretContainer.hasAttribute('data-mce-caret')) {
        trimBogusBr(caretContainer);
        caretContainer.removeAttribute('data-mce-caret');
        caretContainer.removeAttribute('data-mce-bogus');
        caretContainer.removeAttribute('style');
        caretContainer.removeAttribute('_moz_abspos');
        return caretContainer;
      }

      return null;
    };

    return {
      isCaretContainer: isCaretContainer,
      isCaretContainerBlock: isCaretContainerBlock,
      isCaretContainerInline: isCaretContainerInline,
      showCaretContainerBlock: showCaretContainerBlock,
      insertInline: insertInline,
      prependInline: prependInline,
      appendInline: appendInline,
      isBeforeInline: isBeforeInline,
      isAfterInline: isAfterInline,
      insertBlock: insertBlock,
      hasContent: hasContent,
      startsWithCaretContainer: startsWithCaretContainer,
      endsWithCaretContainer: endsWithCaretContainer
    };
  }
);
/**
 * RangeUtils.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
 */

/**
 * This class contains a few utility methods for ranges.
 *
 * @class tinymce.dom.RangeUtils
 */
define(
  'tinymce.core.dom.RangeUtils',
  [
    'tinymce.core.util.Tools',
    'tinymce.core.dom.TreeWalker',
    'tinymce.core.dom.NodeType',
    'tinymce.core.caret.CaretContainer'
  ],
  function (Tools, TreeWalker, NodeType, CaretContainer) {
    var each = Tools.each,
      isContentEditableTrue = NodeType.isContentEditableTrue,
      isContentEditableFalse = NodeType.isContentEditableFalse,
      isCaretContainer = CaretContainer.isCaretContainer;

    var hasCeProperty = function (node) {
      return isContentEditableTrue(node) || isContentEditableFalse(node);
    };

    var getEndChild = function (container, index) {
      var childNodes = container.childNodes;

      index--;

      if (index > childNodes.length - 1) {
        index = childNodes.length - 1;
      } else if (index < 0) {
        index = 0;
      }

      return childNodes[index] || container;
    };

    var findParent = function (node, rootNode, predicate) {
      while (node && node !== rootNode) {
        if (predicate(node)) {
          return node;
        }

        node = node.parentNode;
      }

      return null;
    };

    var hasParent = function (node, rootNode, predicate) {
      return findParent(node, rootNode, predicate) !== null;
    };

    var hasParentWithName = function (node, rootNode, name) {
      return hasParent(node, rootNode, function (node) {
        return node.nodeName === name;
      });
    };

    var isFormatterCaret = function (node) {
      return node.id === '_mce_caret';
    };

    var isCeFalseCaretContainer = function (node, rootNode) {
      return isCaretContainer(node) && hasParent(node, rootNode, isFormatterCaret) === false;
    };

    var RangeUtils = function (dom) {
      /**
       * Walks the specified range like object and executes the callback for each sibling collection it finds.
       *
       * @private
       * @method walk
       * @param {Object} rng Range like object.
       * @param {function} callback Callback function to execute for each sibling collection.
       */
      this.walk = function (rng, callback) {
        var startContainer = rng.startContainer,
          startOffset = rng.startOffset,
          endContainer = rng.endContainer,
          endOffset = rng.endOffset,
          ancestor, startPoint,
          endPoint, node, parent, siblings, nodes;

        // Handle table cell selection the table plugin enables
        // you to fake select table cells and perform formatting actions on them
        nodes = dom.select('td[data-mce-selected],th[data-mce-selected]');
        if (nodes.length > 0) {
          each(nodes, function (node) {
            callback([node]);
          });

          return;
        }

        /**
         * Excludes start/end text node if they are out side the range
         *
         * @private
         * @param {Array} nodes Nodes to exclude items from.
         * @return {Array} Array with nodes excluding the start/end container if needed.
         */
        var exclude = function (nodes) {
          var node;

          // First node is excluded
          node = nodes[0];
          if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) {
            nodes.splice(0, 1);
          }

          // Last node is excluded
          node = nodes[nodes.length - 1];
          if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) {
            nodes.splice(nodes.length - 1, 1);
          }

          return nodes;
        };

        /**
         * Collects siblings
         *
         * @private
         * @param {Node} node Node to collect siblings from.
         * @param {String} name Name of the sibling to check for.
         * @param {Node} endNode
         * @return {Array} Array of collected siblings.
         */
        var collectSiblings = function (node, name, endNode) {
          var siblings = [];

          for (; node && node != endNode; node = node[name]) {
            siblings.push(node);
          }

          return siblings;
        };

        /**
         * Find an end point this is the node just before the common ancestor root.
         *
         * @private
         * @param {Node} node Node to start at.
         * @param {Node} root Root/ancestor element to stop just before.
         * @return {Node} Node just before the root element.
         */
        var findEndPoint = function (node, root) {
          do {
            if (node.parentNode == root) {
              return node;
            }

            node = node.parentNode;
          } while (node);
        };

        var walkBoundary = function (startNode, endNode, next) {
          var siblingName = next ? 'nextSibling' : 'previousSibling';

          for (node = startNode, parent = node.parentNode; node && node != endNode; node = parent) {
            parent = node.parentNode;
            siblings = collectSiblings(node == startNode ? node : node[siblingName], siblingName);

            if (siblings.length) {
              if (!next) {
                siblings.reverse();
              }

              callback(exclude(siblings));
            }
          }
        };

        // If index based start position then resolve it
        if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
          startContainer = startContainer.childNodes[startOffset];
        }

        // If index based end position then resolve it
        if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
          endContainer = getEndChild(endContainer, endOffset);
        }

        // Same container
        if (startContainer == endContainer) {
          return callback(exclude([startContainer]));
        }

        // Find common ancestor and end points
        ancestor = dom.findCommonAncestor(startContainer, endContainer);

        // Process left side
        for (node = startContainer; node; node = node.parentNode) {
          if (node === endContainer) {
            return walkBoundary(startContainer, ancestor, true);
          }

          if (node === ancestor) {
            break;
          }
        }

        // Process right side
        for (node = endContainer; node; node = node.parentNode) {
          if (node === startContainer) {
            return walkBoundary(endContainer, ancestor);
          }

          if (node === ancestor) {
            break;
          }
        }

        // Find start/end point
        startPoint = findEndPoint(startContainer, ancestor) || startContainer;
        endPoint = findEndPoint(endContainer, ancestor) || endContainer;

        // Walk left leaf
        walkBoundary(startContainer, startPoint, true);

        // Walk the middle from start to end point
        siblings = collectSiblings(
          startPoint == startContainer ? startPoint : startPoint.nextSibling,
          'nextSibling',
          endPoint == endContainer ? endPoint.nextSibling : endPoint
        );

        if (siblings.length) {
          callback(exclude(siblings));
        }

        // Walk right leaf
        walkBoundary(endContainer, endPoint);
      };

      /**
       * Splits the specified range at it's start/end points.
       *
       * @private
       * @param {Range/RangeObject} rng Range to split.
       * @return {Object} Range position object.
       */
      this.split = function (rng) {
        var startContainer = rng.startContainer,
          startOffset = rng.startOffset,
          endContainer = rng.endContainer,
          endOffset = rng.endOffset;

        var splitText = function (node, offset) {
          return node.splitText(offset);
        };

        // Handle single text node
        if (startContainer == endContainer && startContainer.nodeType == 3) {
          if (startOffset > 0 && startOffset < startContainer.nodeValue.length) {
            endContainer = splitText(startContainer, startOffset);
            startContainer = endContainer.previousSibling;

            if (endOffset > startOffset) {
              endOffset = endOffset - startOffset;
              startContainer = endContainer = splitText(endContainer, endOffset).previousSibling;
              endOffset = endContainer.nodeValue.length;
              startOffset = 0;
            } else {
              endOffset = 0;
            }
          }
        } else {
          // Split startContainer text node if needed
          if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) {
            startContainer = splitText(startContainer, startOffset);
            startOffset = 0;
          }

          // Split endContainer text node if needed
          if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) {
            endContainer = splitText(endContainer, endOffset).previousSibling;
            endOffset = endContainer.nodeValue.length;
          }
        }

        return {
          startContainer: startContainer,
          startOffset: startOffset,
          endContainer: endContainer,
          endOffset: endOffset
        };
      };

      /**
       * Normalizes the specified range by finding the closest best suitable caret location.
       *
       * @private
       * @param {Range} rng Range to normalize.
       * @return {Boolean} True/false if the specified range was normalized or not.
       */
      this.normalize = function (rng) {
        var normalized = false, collapsed;

        var normalizeEndPoint = function (start) {
          var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap;
          var directionLeft, isAfterNode;

          var isTableCell = function (node) {
            return node && /^(TD|TH|CAPTION)$/.test(node.nodeName);
          };

          var hasBrBeforeAfter = function (node, left) {
            var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body);

            while ((node = walker[left ? 'prev' : 'next']())) {
              if (node.nodeName === "BR") {
                return true;
              }
            }
          };

          var hasContentEditableFalseParent = function (node) {
            while (node && node != body) {
              if (isContentEditableFalse(node)) {
                return true;
              }

              node = node.parentNode;
            }

            return false;
          };

          var isPrevNode = function (node, name) {
            return node.previousSibling && node.previousSibling.nodeName == name;
          };

          // Walks the dom left/right to find a suitable text node to move the endpoint into
          // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG
          var findTextNodeRelative = function (left, startNode) {
            var walker, lastInlineElement, parentBlockContainer;

            startNode = startNode || container;
            parentBlockContainer = dom.getParent(startNode.parentNode, dom.isBlock) || body;

            // Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680
            // This: <p><br>|</p> becomes <p>|<br></p>
            if (left && startNode.nodeName == 'BR' && isAfterNode && dom.isEmpty(parentBlockContainer)) {
              container = startNode.parentNode;
              offset = dom.nodeIndex(startNode);
              normalized = true;
              return;
            }

            // Walk left until we hit a text node we can move to or a block/br/img
            walker = new TreeWalker(startNode, parentBlockContainer);
            while ((node = walker[left ? 'prev' : 'next']())) {
              // Break if we hit a non content editable node
              if (dom.getContentEditableParent(node) === "false" || isCeFalseCaretContainer(node, dom.getRoot())) {
                return;
              }

              // Found text node that has a length
              if (node.nodeType === 3 && node.nodeValue.length > 0) {
                if (hasParentWithName(node, body, 'A') === false) {
                  container = node;
                  offset = left ? node.nodeValue.length : 0;
                  normalized = true;
                }

                return;
              }

              // Break if we find a block or a BR/IMG/INPUT etc
              if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
                return;
              }

              lastInlineElement = node;
            }

            // Only fetch the last inline element when in caret mode for now
            if (collapsed && lastInlineElement) {
              container = lastInlineElement;
              normalized = true;
              offset = 0;
            }
          };

          container = rng[(start ? 'start' : 'end') + 'Container'];
          offset = rng[(start ? 'start' : 'end') + 'Offset'];
          isAfterNode = container.nodeType == 1 && offset === container.childNodes.length;
          nonEmptyElementsMap = dom.schema.getNonEmptyElements();
          directionLeft = start;

          if (isCaretContainer(container)) {
            return;
          }

          if (container.nodeType == 1 && offset > container.childNodes.length - 1) {
            directionLeft = false;
          }

          // If the container is a document move it to the body element
          if (container.nodeType === 9) {
            container = dom.getRoot();
            offset = 0;
          }

          // If the container is body try move it into the closest text node or position
          if (container === body) {
            // If start is before/after a image, table etc
            if (directionLeft) {
              node = container.childNodes[offset > 0 ? offset - 1 : 0];
              if (node) {
                if (isCaretContainer(node)) {
                  return;
                }

                if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") {
                  return;
                }
              }
            }

            // Resolve the index
            if (container.hasChildNodes()) {
              offset = Math.min(!directionLeft && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1);
              container = container.childNodes[offset];
              offset = 0;

              // Don't normalize non collapsed selections like <p>[a</p><table></table>]
              if (!collapsed && container === body.lastChild && container.nodeName === 'TABLE') {
                return;
              }

              if (hasContentEditableFalseParent(container) || isCaretContainer(container)) {
                return;
              }

              // Don't walk into elements that doesn't have any child nodes like a IMG
              if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) {
                // Walk the DOM to find a text node to place the caret at or a BR
                node = container;
                walker = new TreeWalker(container, body);

                do {
                  if (isContentEditableFalse(node) || isCaretContainer(node)) {
                    normalized = false;
                    break;
                  }

                  // Found a text node use that position
                  if (node.nodeType === 3 && node.nodeValue.length > 0) {
                    offset = directionLeft ? 0 : node.nodeValue.length;
                    container = node;
                    normalized = true;
                    break;
                  }

                  // Found a BR/IMG/PRE element that we can place the caret before
                  if (nonEmptyElementsMap[node.nodeName.toLowerCase()] && !isTableCell(node)) {
                    offset = dom.nodeIndex(node);
                    container = node.parentNode;

                    // Put caret after image and pre tag when moving the end point
                    if ((node.nodeName === "IMG" || node.nodeName === "PRE") && !directionLeft) {
                      offset++;
                    }

                    normalized = true;
                    break;
                  }
                } while ((node = (directionLeft ? walker.next() : walker.prev())));
              }
            }
          }

          // Lean the caret to the left if possible
          if (collapsed) {
            // So this: <b>x</b><i>|x</i>
            // Becomes: <b>x|</b><i>x</i>
            // Seems that only gecko has issues with this
            if (container.nodeType === 3 && offset === 0) {
              findTextNodeRelative(true);
            }

            // Lean left into empty inline elements when the caret is before a BR
            // So this: <i><b></b><i>|<br></i>
            // Becomes: <i><b>|</b><i><br></i>
            // Seems that only gecko has issues with this.
            // Special edge case for <p><a>x</a>|<br></p> since we don't want <p><a>x|</a><br></p>
            if (container.nodeType === 1) {
              node = container.childNodes[offset];

              // Offset is after the containers last child
              // then use the previous child for normalization
              if (!node) {
                node = container.childNodes[offset - 1];
              }

              if (node && node.nodeName === 'BR' && !isPrevNode(node, 'A') &&
                !hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) {
                findTextNodeRelative(true, node);
              }
            }
          }

          // Lean the start of the selection right if possible
          // So this: x[<b>x]</b>
          // Becomes: x<b>[x]</b>
          if (directionLeft && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) {
            findTextNodeRelative(false);
          }

          // Set endpoint if it was normalized
          if (normalized) {
            rng['set' + (start ? 'Start' : 'End')](container, offset);
          }
        };

        collapsed = rng.collapsed;

        normalizeEndPoint(true);

        if (!collapsed) {
          normalizeEndPoint();
        }

        // If it was collapsed then make sure it still is
        if (normalized && collapsed) {
          rng.collapse(true);
        }

        return normalized;
      };
    };

    /**
     * Compares two ranges and checks if they are equal.
     *
     * @static
     * @method compareRanges
     * @param {DOMRange} rng1 First range to compare.
     * @param {DOMRange} rng2 First range to compare.
     * @return {Boolean} true/false if the ranges are equal.
     */
    RangeUtils.compareRanges = function (rng1, rng2) {
      return rng1 && rng2 &&
        (rng1.startContainer === rng2.startContainer && rng1.startOffset === rng2.startOffset) &&
        (rng1.endContainer === rng2.endContainer && rng1.endOffset === rng2.endOffset);
    };

    /**
     * Finds the closest selection rect tries to get the range from that.
     */
    var findClosestIeRange = function (clientX, clientY, doc) {
      var element, rng, rects;

      element = doc.elementFromPoint(clientX, clientY);
      rng = doc.body.createTextRange();

      if (!element || element.tagName == 'HTML') {
        element = doc.body;
      }

      rng.moveToElementText(element);
      rects = Tools.toArray(rng.getClientRects());

      rects = rects.sort(function (a, b) {
        a = Math.abs(Math.max(a.top - clientY, a.bottom - clientY));
        b = Math.abs(Math.max(b.top - clientY, b.bottom - clientY));

        return a - b;
      });

      if (rects.length > 0) {
        clientY = (rects[0].bottom + rects[0].top) / 2;

        try {
          rng.moveToPoint(clientX, clientY);
          rng.collapse(true);

          return rng;
        } catch (ex) {
          // At least we tried
        }
      }

      return null;
    };

    var moveOutOfContentEditableFalse = function (rng, rootNode) {
      var parentElement = rng && rng.parentElement ? rng.parentElement() : null;
      return isContentEditableFalse(findParent(parentElement, rootNode, hasCeProperty)) ? null : rng;
    };

    /**
     * Gets the caret range for the given x/y location.
     *
     * @static
     * @method getCaretRangeFromPoint
     * @param {Number} clientX X coordinate for range
     * @param {Number} clientY Y coordinate for range
     * @param {Document} doc Document that x/y are relative to
     * @returns {Range} caret range
     */
    RangeUtils.getCaretRangeFromPoint = function (clientX, clientY, doc) {
      var rng, point;

      if (doc.caretPositionFromPoint) {
        point = doc.caretPositionFromPoint(clientX, clientY);
        rng = doc.createRange();
        rng.setStart(point.offsetNode, point.offset);
        rng.collapse(true);
      } else if (doc.caretRangeFromPoint) {
        rng = doc.caretRangeFromPoint(clientX, clientY);
      } else if (doc.body.createTextRange) {
        rng = doc.body.createTextRange();

        try {
          rng.moveToPoint(clientX, clientY);
          rng.collapse(true);
        } catch (ex) {
          rng = findClosestIeRange(clientX, clientY, doc);
        }

        return moveOutOfContentEditableFalse(rng, doc.body);
      }

      return rng;
    };

    RangeUtils.getSelectedNode = function (range) {
      var startContainer = range.startContainer,
        startOffset = range.startOffset;

      if (startContainer.hasChildNodes() && range.endOffset == startOffset + 1) {
        return startContainer.childNodes[startOffset];
      }

      return null;
    };

    RangeUtils.getNode = function (container, offset) {
      if (container.nodeType === 1 && container.hasChildNodes()) {
        if (offset >= container.childNodes.length) {
          offset = container.childNodes.length - 1;
        }

        container = container.childNodes[offset];
      }

      return container;
    };

    return RangeUtils;
  }
);

/**
 * CaretCandidate.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
 */

/**
 * This module contains logic for handling caret candidates. A caret candidate is
 * for example text nodes, images, input elements, cE=false elements etc.
 *
 * @private
 * @class tinymce.caret.CaretCandidate
 */
define(
  'tinymce.core.caret.CaretCandidate',
  [
    "tinymce.core.dom.NodeType",
    "tinymce.core.util.Arr",
    "tinymce.core.caret.CaretContainer"
  ],
  function (NodeType, Arr, CaretContainer) {
    var isContentEditableTrue = NodeType.isContentEditableTrue,
      isContentEditableFalse = NodeType.isContentEditableFalse,
      isBr = NodeType.isBr,
      isText = NodeType.isText,
      isInvalidTextElement = NodeType.matchNodeNames('script style textarea'),
      isAtomicInline = NodeType.matchNodeNames('img input textarea hr iframe video audio object'),
      isTable = NodeType.matchNodeNames('table'),
      isCaretContainer = CaretContainer.isCaretContainer;

    var isCaretCandidate = function (node) {
      if (isCaretContainer(node)) {
        return false;
      }

      if (isText(node)) {
        if (isInvalidTextElement(node.parentNode)) {
          return false;
        }

        return true;
      }

      return isAtomicInline(node) || isBr(node) || isTable(node) || isContentEditableFalse(node);
    };

    var isInEditable = function (node, rootNode) {
      for (node = node.parentNode; node && node != rootNode; node = node.parentNode) {
        if (isContentEditableFalse(node)) {
          return false;
        }

        if (isContentEditableTrue(node)) {
          return true;
        }
      }

      return true;
    };

    var isAtomicContentEditableFalse = function (node) {
      if (!isContentEditableFalse(node)) {
        return false;
      }

      return Arr.reduce(node.getElementsByTagName('*'), function (result, elm) {
        return result || isContentEditableTrue(elm);
      }, false) !== true;
    };

    var isAtomic = function (node) {
      return isAtomicInline(node) || isAtomicContentEditableFalse(node);
    };

    var isEditableCaretCandidate = function (node, rootNode) {
      return isCaretCandidate(node) && isInEditable(node, rootNode);
    };

    return {
      isCaretCandidate: isCaretCandidate,
      isInEditable: isInEditable,
      isAtomic: isAtomic,
      isEditableCaretCandidate: isEditableCaretCandidate
    };
  }
);
/**
 * ClientRect.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
 */

/**
 * Utility functions for working with client rects.
 *
 * @private
 * @class tinymce.geom.ClientRect
 */
define(
  'tinymce.core.geom.ClientRect',
  [
  ],
  function () {
    var round = Math.round;

    var clone = function (rect) {
      if (!rect) {
        return { left: 0, top: 0, bottom: 0, right: 0, width: 0, height: 0 };
      }

      return {
        left: round(rect.left),
        top: round(rect.top),
        bottom: round(rect.bottom),
        right: round(rect.right),
        width: round(rect.width),
        height: round(rect.height)
      };
    };

    var collapse = function (clientRect, toStart) {
      clientRect = clone(clientRect);

      if (toStart) {
        clientRect.right = clientRect.left;
      } else {
        clientRect.left = clientRect.left + clientRect.width;
        clientRect.right = clientRect.left;
      }

      clientRect.width = 0;

      return clientRect;
    };

    var isEqual = function (rect1, rect2) {
      return (
        rect1.left === rect2.left &&
        rect1.top === rect2.top &&
        rect1.bottom === rect2.bottom &&
        rect1.right === rect2.right
      );
    };

    var isValidOverflow = function (overflowY, clientRect1, clientRect2) {
      return overflowY >= 0 && overflowY <= Math.min(clientRect1.height, clientRect2.height) / 2;

    };

    var isAbove = function (clientRect1, clientRect2) {
      if ((clientRect1.bottom - clientRect1.height / 2) < clientRect2.top) {
        return true;
      }

      if (clientRect1.top > clientRect2.bottom) {
        return false;
      }

      return isValidOverflow(clientRect2.top - clientRect1.bottom, clientRect1, clientRect2);
    };

    var isBelow = function (clientRect1, clientRect2) {
      if (clientRect1.top > clientRect2.bottom) {
        return true;
      }

      if (clientRect1.bottom < clientRect2.top) {
        return false;
      }

      return isValidOverflow(clientRect2.bottom - clientRect1.top, clientRect1, clientRect2);
    };

    var isLeft = function (clientRect1, clientRect2) {
      return clientRect1.left < clientRect2.left;
    };

    var isRight = function (clientRect1, clientRect2) {
      return clientRect1.right > clientRect2.right;
    };

    var compare = function (clientRect1, clientRect2) {
      if (isAbove(clientRect1, clientRect2)) {
        return -1;
      }

      if (isBelow(clientRect1, clientRect2)) {
        return 1;
      }

      if (isLeft(clientRect1, clientRect2)) {
        return -1;
      }

      if (isRight(clientRect1, clientRect2)) {
        return 1;
      }

      return 0;
    };

    var containsXY = function (clientRect, clientX, clientY) {
      return (
        clientX >= clientRect.left &&
        clientX <= clientRect.right &&
        clientY >= clientRect.top &&
        clientY <= clientRect.bottom
      );
    };

    return {
      clone: clone,
      collapse: collapse,
      isEqual: isEqual,
      isAbove: isAbove,
      isBelow: isBelow,
      isLeft: isLeft,
      isRight: isRight,
      compare: compare,
      containsXY: containsXY
    };
  }
);

/**
 * ExtendingChar.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
 */

/**
 * This class contains logic for detecting extending characters.
 *
 * @private
 * @class tinymce.text.ExtendingChar
 * @example
 * var isExtending = ExtendingChar.isExtendingChar('a');
 */
define(
  'tinymce.core.text.ExtendingChar',
  [
  ],
  function () {
    // Generated from: http://www.unicode.org/Public/UNIDATA/DerivedCoreProperties.txt
    // Only includes the characters in that fit into UCS-2 16 bit
    var extendingChars = new RegExp(
      "[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A" +
      "\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0" +
      "\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08E3-\u0902\u093A\u093C" +
      "\u0941-\u0948\u094D\u0951-\u0957\u0962-\u0963\u0981\u09BC\u09BE\u09C1-\u09C4\u09CD\u09D7\u09E2-\u09E3" +
      "\u0A01-\u0A02\u0A3C\u0A41-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D\u0A51\u0A70-\u0A71\u0A75\u0A81-\u0A82\u0ABC" +
      "\u0AC1-\u0AC5\u0AC7-\u0AC8\u0ACD\u0AE2-\u0AE3\u0B01\u0B3C\u0B3E\u0B3F\u0B41-\u0B44\u0B4D\u0B56\u0B57" +
      "\u0B62-\u0B63\u0B82\u0BBE\u0BC0\u0BCD\u0BD7\u0C00\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56" +
      "\u0C62-\u0C63\u0C81\u0CBC\u0CBF\u0CC2\u0CC6\u0CCC-\u0CCD\u0CD5-\u0CD6\u0CE2-\u0CE3\u0D01\u0D3E\u0D41-\u0D44" +
      "\u0D4D\u0D57\u0D62-\u0D63\u0DCA\u0DCF\u0DD2-\u0DD4\u0DD6\u0DDF\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9" +
      "\u0EBB-\u0EBC\u0EC8-\u0ECD\u0F18-\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86-\u0F87\u0F8D-\u0F97" +
      "\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039-\u103A\u103D-\u103E\u1058-\u1059\u105E-\u1060\u1071-\u1074" +
      "\u1082\u1085-\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17B4-\u17B5" +
      "\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u18A9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193B\u1A17-\u1A18" +
      "\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ABD\u1ABE\u1B00-\u1B03\u1B34" +
      "\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80-\u1B81\u1BA2-\u1BA5\u1BA8-\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8-\u1BE9" +
      "\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8-\u1CF9" +
      "\u1DC0-\u1DF5\u1DFC-\u1DFF\u200C-\u200D\u20D0-\u20DC\u20DD-\u20E0\u20E1\u20E2-\u20E4\u20E5-\u20F0\u2CEF-\u2CF1" +
      "\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u302E-\u302F\u3099-\u309A\uA66F\uA670-\uA672\uA674-\uA67D\uA69E-\uA69F\uA6F0-\uA6F1" +
      "\uA802\uA806\uA80B\uA825-\uA826\uA8C4\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC" +
      "\uA9E5\uAA29-\uAA2E\uAA31-\uAA32\uAA35-\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7-\uAAB8\uAABE-\uAABF\uAAC1" +
      "\uAAEC-\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFF9E-\uFF9F]"
    );

    var isExtendingChar = function (ch) {
      return typeof ch == "string" && ch.charCodeAt(0) >= 768 && extendingChars.test(ch);
    };

    return {
      isExtendingChar: isExtendingChar
    };
  }
);
/**
 * CaretPosition.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
 */

/**
 * This module contains logic for creating caret positions within a document a caretposition
 * is similar to a DOMRange object but it doesn't have two endpoints and is also more lightweight
 * since it's now updated live when the DOM changes.
 *
 * @private
 * @class tinymce.caret.CaretPosition
 * @example
 * var caretPos1 = new CaretPosition(container, offset);
 * var caretPos2 = CaretPosition.fromRangeStart(someRange);
 */
define(
  'tinymce.core.caret.CaretPosition',
  [
    "tinymce.core.util.Fun",
    "tinymce.core.dom.NodeType",
    "tinymce.core.dom.DOMUtils",
    "tinymce.core.dom.RangeUtils",
    "tinymce.core.caret.CaretCandidate",
    "tinymce.core.geom.ClientRect",
    "tinymce.core.text.ExtendingChar"
  ],
  function (Fun, NodeType, DOMUtils, RangeUtils, CaretCandidate, ClientRect, ExtendingChar) {
    var isElement = NodeType.isElement,
      isCaretCandidate = CaretCandidate.isCaretCandidate,
      isBlock = NodeType.matchStyleValues('display', 'block table'),
      isFloated = NodeType.matchStyleValues('float', 'left right'),
      isValidElementCaretCandidate = Fun.and(isElement, isCaretCandidate, Fun.negate(isFloated)),
      isNotPre = Fun.negate(NodeType.matchStyleValues('white-space', 'pre pre-line pre-wrap')),
      isText = NodeType.isText,
      isBr = NodeType.isBr,
      nodeIndex = DOMUtils.nodeIndex,
      resolveIndex = RangeUtils.getNode;

    var createRange = function (doc) {
      return "createRange" in doc ? doc.createRange() : DOMUtils.DOM.createRng();
    };

    var isWhiteSpace = function (chr) {
      return chr && /[\r\n\t ]/.test(chr);
    };

    var isHiddenWhiteSpaceRange = function (range) {
      var container = range.startContainer,
        offset = range.startOffset,
        text;

      if (isWhiteSpace(range.toString()) && isNotPre(container.parentNode)) {
        text = container.data;

        if (isWhiteSpace(text[offset - 1]) || isWhiteSpace(text[offset + 1])) {
          return true;
        }
      }

      return false;
    };

    var getCaretPositionClientRects = function (caretPosition) {
      var clientRects = [], beforeNode, node;

      // Hack for older WebKit versions that doesn't
      // support getBoundingClientRect on BR elements
      var getBrClientRect = function (brNode) {
        var doc = brNode.ownerDocument,
          rng = createRange(doc),
          nbsp = doc.createTextNode('\u00a0'),
          parentNode = brNode.parentNode,
          clientRect;

        parentNode.insertBefore(nbsp, brNode);
        rng.setStart(nbsp, 0);
        rng.setEnd(nbsp, 1);
        clientRect = ClientRect.clone(rng.getBoundingClientRect());
        parentNode.removeChild(nbsp);

        return clientRect;
      };

      var getBoundingClientRect = function (item) {
        var clientRect, clientRects;

        clientRects = item.getClientRects();
        if (clientRects.length > 0) {
          clientRect = ClientRect.clone(clientRects[0]);
        } else {
          clientRect = ClientRect.clone(item.getBoundingClientRect());
        }

        if (isBr(item) && clientRect.left === 0) {
          return getBrClientRect(item);
        }

        return clientRect;
      };

      var collapseAndInflateWidth = function (clientRect, toStart) {
        clientRect = ClientRect.collapse(clientRect, toStart);
        clientRect.width = 1;
        clientRect.right = clientRect.left + 1;

        return clientRect;
      };

      var addUniqueAndValidRect = function (clientRect) {
        if (clientRect.height === 0) {
          return;
        }

        if (clientRects.length > 0) {
          if (ClientRect.isEqual(clientRect, clientRects[clientRects.length - 1])) {
            return;
          }
        }

        clientRects.push(clientRect);
      };

      var addCharacterOffset = function (container, offset) {
        var range = createRange(container.ownerDocument);

        if (offset < container.data.length) {
          if (ExtendingChar.isExtendingChar(container.data[offset])) {
            return clientRects;
          }

          // WebKit returns two client rects for a position after an extending
          // character a\uxxx|b so expand on "b" and collapse to start of "b" box
          if (ExtendingChar.isExtendingChar(container.data[offset - 1])) {
            range.setStart(container, offset);
            range.setEnd(container, offset + 1);

            if (!isHiddenWhiteSpaceRange(range)) {
              addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), false));
              return clientRects;
            }
          }
        }

        if (offset > 0) {
          range.setStart(container, offset - 1);
          range.setEnd(container, offset);

          if (!isHiddenWhiteSpaceRange(range)) {
            addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), false));
          }
        }

        if (offset < container.data.length) {
          range.setStart(container, offset);
          range.setEnd(container, offset + 1);

          if (!isHiddenWhiteSpaceRange(range)) {
            addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), true));
          }
        }
      };

      if (isText(caretPosition.container())) {
        addCharacterOffset(caretPosition.container(), caretPosition.offset());
        return clientRects;
      }

      if (isElement(caretPosition.container())) {
        if (caretPosition.isAtEnd()) {
          node = resolveIndex(caretPosition.container(), caretPosition.offset());
          if (isText(node)) {
            addCharacterOffset(node, node.data.length);
          }

          if (isValidElementCaretCandidate(node) && !isBr(node)) {
            addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), false));
          }
        } else {
          node = resolveIndex(caretPosition.container(), caretPosition.offset());
          if (isText(node)) {
            addCharacterOffset(node, 0);
          }

          if (isValidElementCaretCandidate(node) && caretPosition.isAtEnd()) {
            addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), false));
            return clientRects;
          }

          beforeNode = resolveIndex(caretPosition.container(), caretPosition.offset() - 1);
          if (isValidElementCaretCandidate(beforeNode) && !isBr(beforeNode)) {
            if (isBlock(beforeNode) || isBlock(node) || !isValidElementCaretCandidate(node)) {
              addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(beforeNode), false));
            }
          }

          if (isValidElementCaretCandidate(node)) {
            addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), true));
          }
        }
      }

      return clientRects;
    };

    /**
     * Represents a location within the document by a container and an offset.
     *
     * @constructor
     * @param {Node} container Container node.
     * @param {Number} offset Offset within that container node.
     * @param {Array} clientRects Optional client rects array for the position.
     */
    var CaretPosition = function (container, offset, clientRects) {
      var isAtStart = function () {
        if (isText(container)) {
          return offset === 0;
        }

        return offset === 0;
      };

      var isAtEnd = function () {
        if (isText(container)) {
          return offset >= container.data.length;
        }

        return offset >= container.childNodes.length;
      };

      var toRange = function () {
        var range;

        range = createRange(container.ownerDocument);
        range.setStart(container, offset);
        range.setEnd(container, offset);

        return range;
      };

      var getClientRects = function () {
        if (!clientRects) {
          clientRects = getCaretPositionClientRects(new CaretPosition(container, offset));
        }

        return clientRects;
      };

      var isVisible = function () {
        return getClientRects().length > 0;
      };

      var isEqual = function (caretPosition) {
        return caretPosition && container === caretPosition.container() && offset === caretPosition.offset();
      };

      var getNode = function (before) {
        return resolveIndex(container, before ? offset - 1 : offset);
      };

      return {
        /**
         * Returns the container node.
         *
         * @method container
         * @return {Node} Container node.
         */
        container: Fun.constant(container),

        /**
         * Returns the offset within the container node.
         *
         * @method offset
         * @return {Number} Offset within the container node.
         */
        offset: Fun.constant(offset),

        /**
         * Returns a range out of a the caret position.
         *
         * @method toRange
         * @return {DOMRange} range for the caret position.
         */
        toRange: toRange,

        /**
         * Returns the client rects for the caret position. Might be multiple rects between
         * block elements.
         *
         * @method getClientRects
         * @return {Array} Array of client rects.
         */
        getClientRects: getClientRects,

        /**
         * Returns true if the caret location is visible/displayed on screen.
         *
         * @method isVisible
         * @return {Boolean} true/false if the position is visible or not.
         */
        isVisible: isVisible,

        /**
         * Returns true if the caret location is at the beginning of text node or container.
         *
         * @method isVisible
         * @return {Boolean} true/false if the position is at the beginning.
         */
        isAtStart: isAtStart,

        /**
         * Returns true if the caret location is at the end of text node or container.
         *
         * @method isVisible
         * @return {Boolean} true/false if the position is at the end.
         */
        isAtEnd: isAtEnd,

        /**
         * Compares the caret position to another caret position. This will only compare the
         * container and offset not it's visual position.
         *
         * @method isEqual
         * @param {tinymce.caret.CaretPosition} caretPosition Caret position to compare with.
         * @return {Boolean} true if the caret positions are equal.
         */
        isEqual: isEqual,

        /**
         * Returns the closest resolved node from a node index. That means if you have an offset after the
         * last node in a container it will return that last node.
         *
         * @method getNode
         * @return {Node} Node that is closest to the index.
         */
        getNode: getNode
      };
    };

    /**
     * Creates a caret position from the start of a range.
     *
     * @method fromRangeStart
     * @param {DOMRange} range DOM Range to create caret position from.
     * @return {tinymce.caret.CaretPosition} Caret position from the start of DOM range.
     */
    CaretPosition.fromRangeStart = function (range) {
      return new CaretPosition(range.startContainer, range.startOffset);
    };

    /**
     * Creates a caret position from the end of a range.
     *
     * @method fromRangeEnd
     * @param {DOMRange} range DOM Range to create caret position from.
     * @return {tinymce.caret.CaretPosition} Caret position from the end of DOM range.
     */
    CaretPosition.fromRangeEnd = function (range) {
      return new CaretPosition(range.endContainer, range.endOffset);
    };

    /**
     * Creates a caret position from a node and places the offset after it.
     *
     * @method after
     * @param {Node} node Node to get caret position from.
     * @return {tinymce.caret.CaretPosition} Caret position from the node.
     */
    CaretPosition.after = function (node) {
      return new CaretPosition(node.parentNode, nodeIndex(node) + 1);
    };

    /**
     * Creates a caret position from a node and places the offset before it.
     *
     * @method before
     * @param {Node} node Node to get caret position from.
     * @return {tinymce.caret.CaretPosition} Caret position from the node.
     */
    CaretPosition.before = function (node) {
      return new CaretPosition(node.parentNode, nodeIndex(node));
    };

    CaretPosition.isAtStart = function (pos) {
      return pos ? pos.isAtStart() : false;
    };

    CaretPosition.isAtEnd = function (pos) {
      return pos ? pos.isAtEnd() : false;
    };

    CaretPosition.isTextPosition = function (pos) {
      return pos ? NodeType.isText(pos.container()) : false;
    };

    return CaretPosition;
  }
);
/**
 * CaretBookmark.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
 */

/**
 * This module creates or resolves xpath like string representation of a CaretPositions.
 *
 * The format is a / separated list of chunks with:
 * <element|text()>[index|after|before]
 *
 * For example:
 *  p[0]/b[0]/text()[0],1 = <p><b>a|c</b></p>
 *  p[0]/img[0],before = <p>|<img></p>
 *  p[0]/img[0],after = <p><img>|</p>
 *
 * @private
 * @static
 * @class tinymce.caret.CaretBookmark
 * @example
 * var bookmark = CaretBookmark.create(rootElm, CaretPosition.before(rootElm.firstChild));
 * var caretPosition = CaretBookmark.resolve(bookmark);
 */
define(
  'tinymce.core.caret.CaretBookmark',
  [
    'tinymce.core.dom.NodeType',
    'tinymce.core.dom.DOMUtils',
    'tinymce.core.util.Fun',
    'tinymce.core.util.Arr',
    'tinymce.core.caret.CaretPosition'
  ],
  function (NodeType, DomUtils, Fun, Arr, CaretPosition) {
    var isText = NodeType.isText,
      isBogus = NodeType.isBogus,
      nodeIndex = DomUtils.nodeIndex;

    var normalizedParent = function (node) {
      var parentNode = node.parentNode;

      if (isBogus(parentNode)) {
        return normalizedParent(parentNode);
      }

      return parentNode;
    };

    var getChildNodes = function (node) {
      if (!node) {
        return [];
      }

      return Arr.reduce(node.childNodes, function (result, node) {
        if (isBogus(node) && node.nodeName != 'BR') {
          result = result.concat(getChildNodes(node));
        } else {
          result.push(node);
        }

        return result;
      }, []);
    };

    var normalizedTextOffset = function (textNode, offset) {
      while ((textNode = textNode.previousSibling)) {
        if (!isText(textNode)) {
          break;
        }

        offset += textNode.data.length;
      }

      return offset;
    };

    var equal = function (targetValue) {
      return function (value) {
        return targetValue === value;
      };
    };

    var normalizedNodeIndex = function (node) {
      var nodes, index, numTextFragments;

      nodes = getChildNodes(normalizedParent(node));
      index = Arr.findIndex(nodes, equal(node), node);
      nodes = nodes.slice(0, index + 1);
      numTextFragments = Arr.reduce(nodes, function (result, node, i) {
        if (isText(node) && isText(nodes[i - 1])) {
          result++;
        }

        return result;
      }, 0);

      nodes = Arr.filter(nodes, NodeType.matchNodeNames(node.nodeName));
      index = Arr.findIndex(nodes, equal(node), node);

      return index - numTextFragments;
    };

    var createPathItem = function (node) {
      var name;

      if (isText(node)) {
        name = 'text()';
      } else {
        name = node.nodeName.toLowerCase();
      }

      return name + '[' + normalizedNodeIndex(node) + ']';
    };

    var parentsUntil = function (rootNode, node, predicate) {
      var parents = [];

      for (node = node.parentNode; node != rootNode; node = node.parentNode) {
        if (predicate && predicate(node)) {
          break;
        }

        parents.push(node);
      }

      return parents;
    };

    var create = function (rootNode, caretPosition) {
      var container, offset, path = [],
        outputOffset, childNodes, parents;

      container = caretPosition.container();
      offset = caretPosition.offset();

      if (isText(container)) {
        outputOffset = normalizedTextOffset(container, offset);
      } else {
        childNodes = container.childNodes;
        if (offset >= childNodes.length) {
          outputOffset = 'after';
          offset = childNodes.length - 1;
        } else {
          outputOffset = 'before';
        }

        container = childNodes[offset];
      }

      path.push(createPathItem(container));
      parents = parentsUntil(rootNode, container);
      parents = Arr.filter(parents, Fun.negate(NodeType.isBogus));
      path = path.concat(Arr.map(parents, function (node) {
        return createPathItem(node);
      }));

      return path.reverse().join('/') + ',' + outputOffset;
    };

    var resolvePathItem = function (node, name, index) {
      var nodes = getChildNodes(node);

      nodes = Arr.filter(nodes, function (node, index) {
        return !isText(node) || !isText(nodes[index - 1]);
      });

      nodes = Arr.filter(nodes, NodeType.matchNodeNames(name));
      return nodes[index];
    };

    var findTextPosition = function (container, offset) {
      var node = container, targetOffset = 0, dataLen;

      while (isText(node)) {
        dataLen = node.data.length;

        if (offset >= targetOffset && offset <= targetOffset + dataLen) {
          container = node;
          offset = offset - targetOffset;
          break;
        }

        if (!isText(node.nextSibling)) {
          container = node;
          offset = dataLen;
          break;
        }

        targetOffset += dataLen;
        node = node.nextSibling;
      }

      if (offset > container.data.length) {
        offset = container.data.length;
      }

      return new CaretPosition(container, offset);
    };

    var resolve = function (rootNode, path) {
      var parts, container, offset;

      if (!path) {
        return null;
      }

      parts = path.split(',');
      path = parts[0].split('/');
      offset = parts.length > 1 ? parts[1] : 'before';

      container = Arr.reduce(path, function (result, value) {
        value = /([\w\-\(\)]+)\[([0-9]+)\]/.exec(value);
        if (!value) {
          return null;
        }

        if (value[1] === 'text()') {
          value[1] = '#text';
        }

        return resolvePathItem(result, value[1], parseInt(value[2], 10));
      }, rootNode);

      if (!container) {
        return null;
      }

      if (!isText(container)) {
        if (offset === 'after') {
          offset = nodeIndex(container) + 1;
        } else {
          offset = nodeIndex(container);
        }

        return new CaretPosition(container.parentNode, offset);
      }

      return findTextPosition(container, parseInt(offset, 10));
    };

    return {
      /**
       * Create a xpath bookmark location for the specified caret position.
       *
       * @method create
       * @param {Node} rootNode Root node to create bookmark within.
       * @param {tinymce.caret.CaretPosition} caretPosition Caret position within the root node.
       * @return {String} String xpath like location of caret position.
       */
      create: create,

      /**
       * Resolves a xpath like bookmark location to the a caret position.
       *
       * @method resolve
       * @param {Node} rootNode Root node to resolve xpath bookmark within.
       * @param {String} bookmark Bookmark string to resolve.
       * @return {tinymce.caret.CaretPosition} Caret position resolved from xpath like bookmark.
       */
      resolve: resolve
    };
  }
);
/**
 * BookmarkManager.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
 */

/**
 * This class handles selection bookmarks.
 *
 * @class tinymce.dom.BookmarkManager
 */
define(
  'tinymce.core.dom.BookmarkManager',
  [
    'tinymce.core.caret.CaretBookmark',
    'tinymce.core.caret.CaretContainer',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.dom.NodeType',
    'tinymce.core.dom.RangeUtils',
    'tinymce.core.Env',
    'tinymce.core.text.Zwsp',
    'tinymce.core.util.Tools'
  ],
  function (CaretBookmark, CaretContainer, CaretPosition, NodeType, RangeUtils, Env, Zwsp, Tools) {
    var isContentEditableFalse = NodeType.isContentEditableFalse;

    var getNormalizedTextOffset = function (container, offset) {
      var node, trimmedOffset;

      trimmedOffset = Zwsp.trim(container.data.slice(0, offset)).length;
      for (node = container.previousSibling; node && node.nodeType === 3; node = node.previousSibling) {
        trimmedOffset += Zwsp.trim(node.data).length;
      }

      return trimmedOffset;
    };

    var trimEmptyTextNode = function (node) {
      if (NodeType.isText(node) && node.data.length === 0) {
        node.parentNode.removeChild(node);
      }
    };

    /**
     * Constructs a new BookmarkManager instance for a specific selection instance.
     *
     * @constructor
     * @method BookmarkManager
     * @param {tinymce.dom.Selection} selection Selection instance to handle bookmarks for.
     */
    var BookmarkManager = function (selection) {
      var dom = selection.dom;

      /**
       * Returns a bookmark location for the current selection. This bookmark object
       * can then be used to restore the selection after some content modification to the document.
       *
       * @method getBookmark
       * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex.
       * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization.
       * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection.
       * @example
       * // Stores a bookmark of the current selection
       * var bm = tinymce.activeEditor.selection.getBookmark();
       *
       * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
       *
       * // Restore the selection bookmark
       * tinymce.activeEditor.selection.moveToBookmark(bm);
       */
      this.getBookmark = function (type, normalized) {
        var rng, rng2, id, collapsed, name, element, chr = '&#xFEFF;', styles;

        var findIndex = function (name, element) {
          var count = 0;

          Tools.each(dom.select(name), function (node) {
            if (node.getAttribute('data-mce-bogus') === 'all') {
              return;
            }

            if (node == element) {
              return false;
            }

            count++;
          });

          return count;
        };

        var normalizeTableCellSelection = function (rng) {
          var moveEndPoint = function (start) {
            var container, offset, childNodes, prefix = start ? 'start' : 'end';

            container = rng[prefix + 'Container'];
            offset = rng[prefix + 'Offset'];

            if (container.nodeType == 1 && container.nodeName == "TR") {
              childNodes = container.childNodes;
              container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)];
              if (container) {
                offset = start ? 0 : container.childNodes.length;
                rng['set' + (start ? 'Start' : 'End')](container, offset);
              }
            }
          };

          moveEndPoint(true);
          moveEndPoint();

          return rng;
        };

        var getLocation = function (rng) {
          var root = dom.getRoot(), bookmark = {};

          var getPoint = function (rng, start) {
            var container = rng[start ? 'startContainer' : 'endContainer'],
              offset = rng[start ? 'startOffset' : 'endOffset'], point = [], childNodes, after = 0;

            if (container.nodeType === 3) {
              point.push(normalized ? getNormalizedTextOffset(container, offset) : offset);
            } else {
              childNodes = container.childNodes;

              if (offset >= childNodes.length && childNodes.length) {
                after = 1;
                offset = Math.max(0, childNodes.length - 1);
              }

              point.push(dom.nodeIndex(childNodes[offset], normalized) + after);
            }

            for (; container && container != root; container = container.parentNode) {
              point.push(dom.nodeIndex(container, normalized));
            }

            return point;
          };

          bookmark.start = getPoint(rng, true);

          if (!selection.isCollapsed()) {
            bookmark.end = getPoint(rng);
          }

          return bookmark;
        };

        var findAdjacentContentEditableFalseElm = function (rng) {
          var findSibling = function (node, offset) {
            var sibling;

            if (NodeType.isElement(node)) {
              node = RangeUtils.getNode(node, offset);
              if (isContentEditableFalse(node)) {
                return node;
              }
            }

            if (CaretContainer.isCaretContainer(node)) {
              if (NodeType.isText(node) && CaretContainer.isCaretContainerBlock(node)) {
                node = node.parentNode;
              }

              sibling = node.previousSibling;
              if (isContentEditableFalse(sibling)) {
                return sibling;
              }

              sibling = node.nextSibling;
              if (isContentEditableFalse(sibling)) {
                return sibling;
              }
            }
          };

          return findSibling(rng.startContainer, rng.startOffset) || findSibling(rng.endContainer, rng.endOffset);
        };

        if (type == 2) {
          element = selection.getNode();
          name = element ? element.nodeName : null;
          rng = selection.getRng();

          if (isContentEditableFalse(element) || name == 'IMG') {
            return { name: name, index: findIndex(name, element) };
          }

          element = findAdjacentContentEditableFalseElm(rng);
          if (element) {
            name = element.tagName;
            return { name: name, index: findIndex(name, element) };
          }

          return getLocation(rng);
        }

        if (type == 3) {
          rng = selection.getRng();

          return {
            start: CaretBookmark.create(dom.getRoot(), CaretPosition.fromRangeStart(rng)),
            end: CaretBookmark.create(dom.getRoot(), CaretPosition.fromRangeEnd(rng))
          };
        }

        // Handle simple range
        if (type) {
          return { rng: selection.getRng() };
        }

        rng = selection.getRng();
        id = dom.uniqueId();
        collapsed = selection.isCollapsed();
        styles = 'overflow:hidden;line-height:0px';
        element = selection.getNode();
        name = element.nodeName;
        if (name == 'IMG') {
          return { name: name, index: findIndex(name, element) };
        }

        // W3C method
        rng2 = normalizeTableCellSelection(rng.cloneRange());

        // Insert end marker
        if (!collapsed) {
          rng2.collapse(false);
          var endBookmarkNode = dom.create('span', { 'data-mce-type': "bookmark", id: id + '_end', style: styles }, chr);
          rng2.insertNode(endBookmarkNode);
          trimEmptyTextNode(endBookmarkNode.nextSibling);
        }

        rng = normalizeTableCellSelection(rng);
        rng.collapse(true);
        var startBookmarkNode = dom.create('span', { 'data-mce-type': "bookmark", id: id + '_start', style: styles }, chr);
        rng.insertNode(startBookmarkNode);
        trimEmptyTextNode(startBookmarkNode.previousSibling);

        selection.moveToBookmark({ id: id, keep: 1 });

        return { id: id };
      };

      /**
       * Restores the selection to the specified bookmark.
       *
       * @method moveToBookmark
       * @param {Object} bookmark Bookmark to restore selection from.
       * @return {Boolean} true/false if it was successful or not.
       * @example
       * // Stores a bookmark of the current selection
       * var bm = tinymce.activeEditor.selection.getBookmark();
       *
       * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
       *
       * // Restore the selection bookmark
       * tinymce.activeEditor.selection.moveToBookmark(bm);
       */
      this.moveToBookmark = function (bookmark) {
        var rng, root, startContainer, endContainer, startOffset, endOffset;

        var setEndPoint = function (start) {
          var point = bookmark[start ? 'start' : 'end'], i, node, offset, children;

          if (point) {
            offset = point[0];

            // Find container node
            for (node = root, i = point.length - 1; i >= 1; i--) {
              children = node.childNodes;

              if (point[i] > children.length - 1) {
                return;
              }

              node = children[point[i]];
            }

            // Move text offset to best suitable location
            if (node.nodeType === 3) {
              offset = Math.min(point[0], node.nodeValue.length);
            }

            // Move element offset to best suitable location
            if (node.nodeType === 1) {
              offset = Math.min(point[0], node.childNodes.length);
            }

            // Set offset within container node
            if (start) {
              rng.setStart(node, offset);
            } else {
              rng.setEnd(node, offset);
            }
          }

          return true;
        };

        var restoreEndPoint = function (suffix) {
          var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep;

          if (marker) {
            node = marker.parentNode;

            if (suffix == 'start') {
              if (!keep) {
                idx = dom.nodeIndex(marker);
              } else {
                node = marker.firstChild;
                idx = 1;
              }

              startContainer = endContainer = node;
              startOffset = endOffset = idx;
            } else {
              if (!keep) {
                idx = dom.nodeIndex(marker);
              } else {
                node = marker.firstChild;
                idx = 1;
              }

              endContainer = node;
              endOffset = idx;
            }

            if (!keep) {
              prev = marker.previousSibling;
              next = marker.nextSibling;

              // Remove all marker text nodes
              Tools.each(Tools.grep(marker.childNodes), function (node) {
                if (node.nodeType == 3) {
                  node.nodeValue = node.nodeValue.replace(/\uFEFF/g, '');
                }
              });

              // Remove marker but keep children if for example contents where inserted into the marker
              // Also remove duplicated instances of the marker for example by a
              // split operation or by WebKit auto split on paste feature
              while ((marker = dom.get(bookmark.id + '_' + suffix))) {
                dom.remove(marker, 1);
              }

              // If siblings are text nodes then merge them unless it's Opera since it some how removes the node
              // and we are sniffing since adding a lot of detection code for a browser with 3% of the market
              // isn't worth the effort. Sorry, Opera but it's just a fact
              if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !Env.opera) {
                idx = prev.nodeValue.length;
                prev.appendData(next.nodeValue);
                dom.remove(next);

                if (suffix == 'start') {
                  startContainer = endContainer = prev;
                  startOffset = endOffset = idx;
                } else {
                  endContainer = prev;
                  endOffset = idx;
                }
              }
            }
          }
        };

        var addBogus = function (node) {
          // Adds a bogus BR element for empty block elements
          if (dom.isBlock(node) && !node.innerHTML && !Env.ie) {
            node.innerHTML = '<br data-mce-bogus="1" />';
          }

          return node;
        };

        var resolveCaretPositionBookmark = function () {
          var rng, pos;

          rng = dom.createRng();
          pos = CaretBookmark.resolve(dom.getRoot(), bookmark.start);
          rng.setStart(pos.container(), pos.offset());

          pos = CaretBookmark.resolve(dom.getRoot(), bookmark.end);
          rng.setEnd(pos.container(), pos.offset());

          return rng;
        };

        if (bookmark) {
          if (Tools.isArray(bookmark.start)) {
            rng = dom.createRng();
            root = dom.getRoot();

            if (setEndPoint(true) && setEndPoint()) {
              selection.setRng(rng);
            }
          } else if (typeof bookmark.start == 'string') {
            selection.setRng(resolveCaretPositionBookmark(bookmark));
          } else if (bookmark.id) {
            // Restore start/end points
            restoreEndPoint('start');
            restoreEndPoint('end');

            if (startContainer) {
              rng = dom.createRng();
              rng.setStart(addBogus(startContainer), startOffset);
              rng.setEnd(addBogus(endContainer), endOffset);
              selection.setRng(rng);
            }
          } else if (bookmark.name) {
            selection.select(dom.select(bookmark.name)[bookmark.index]);
          } else if (bookmark.rng) {
            selection.setRng(bookmark.rng);
          }
        }
      };
    };

    /**
     * Returns true/false if the specified node is a bookmark node or not.
     *
     * @static
     * @method isBookmarkNode
     * @param {DOMNode} node DOM Node to check if it's a bookmark node or not.
     * @return {Boolean} true/false if the node is a bookmark node or not.
     */
    BookmarkManager.isBookmarkNode = function (node) {
      return node && node.tagName === 'SPAN' && node.getAttribute('data-mce-type') === 'bookmark';
    };

    return BookmarkManager;
  }
);
/**
 * CaretUtils.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
 */

/**
 * Utility functions shared by the caret logic.
 *
 * @private
 * @class tinymce.caret.CaretUtils
 */
define(
  'tinymce.core.caret.CaretUtils',
  [
    "tinymce.core.util.Fun",
    "tinymce.core.dom.TreeWalker",
    "tinymce.core.dom.NodeType",
    "tinymce.core.caret.CaretPosition",
    "tinymce.core.caret.CaretContainer",
    "tinymce.core.caret.CaretCandidate"
  ],
  function (Fun, TreeWalker, NodeType, CaretPosition, CaretContainer, CaretCandidate) {
    var isContentEditableTrue = NodeType.isContentEditableTrue,
      isContentEditableFalse = NodeType.isContentEditableFalse,
      isBlockLike = NodeType.matchStyleValues('display', 'block table table-cell table-caption list-item'),
      isCaretContainer = CaretContainer.isCaretContainer,
      isCaretContainerBlock = CaretContainer.isCaretContainerBlock,
      curry = Fun.curry,
      isElement = NodeType.isElement,
      isCaretCandidate = CaretCandidate.isCaretCandidate;

    var isForwards = function (direction) {
      return direction > 0;
    };

    var isBackwards = function (direction) {
      return direction < 0;
    };

    var skipCaretContainers = function (walk, shallow) {
      var node;

      while ((node = walk(shallow))) {
        if (!isCaretContainerBlock(node)) {
          return node;
        }
      }

      return null;
    };

    var findNode = function (node, direction, predicateFn, rootNode, shallow) {
      var walker = new TreeWalker(node, rootNode);

      if (isBackwards(direction)) {
        if (isContentEditableFalse(node) || isCaretContainerBlock(node)) {
          node = skipCaretContainers(walker.prev, true);
          if (predicateFn(node)) {
            return node;
          }
        }

        while ((node = skipCaretContainers(walker.prev, shallow))) {
          if (predicateFn(node)) {
            return node;
          }
        }
      }

      if (isForwards(direction)) {
        if (isContentEditableFalse(node) || isCaretContainerBlock(node)) {
          node = skipCaretContainers(walker.next, true);
          if (predicateFn(node)) {
            return node;
          }
        }

        while ((node = skipCaretContainers(walker.next, shallow))) {
          if (predicateFn(node)) {
            return node;
          }
        }
      }

      return null;
    };

    var getEditingHost = function (node, rootNode) {
      for (node = node.parentNode; node && node != rootNode; node = node.parentNode) {
        if (isContentEditableTrue(node)) {
          return node;
        }
      }

      return rootNode;
    };

    var getParentBlock = function (node, rootNode) {
      while (node && node != rootNode) {
        if (isBlockLike(node)) {
          return node;
        }

        node = node.parentNode;
      }

      return null;
    };

    var isInSameBlock = function (caretPosition1, caretPosition2, rootNode) {
      return getParentBlock(caretPosition1.container(), rootNode) == getParentBlock(caretPosition2.container(), rootNode);
    };

    var isInSameEditingHost = function (caretPosition1, caretPosition2, rootNode) {
      return getEditingHost(caretPosition1.container(), rootNode) == getEditingHost(caretPosition2.container(), rootNode);
    };

    var getChildNodeAtRelativeOffset = function (relativeOffset, caretPosition) {
      var container, offset;

      if (!caretPosition) {
        return null;
      }

      container = caretPosition.container();
      offset = caretPosition.offset();

      if (!isElement(container)) {
        return null;
      }

      return container.childNodes[offset + relativeOffset];
    };

    var beforeAfter = function (before, node) {
      var range = node.ownerDocument.createRange();

      if (before) {
        range.setStartBefore(node);
        range.setEndBefore(node);
      } else {
        range.setStartAfter(node);
        range.setEndAfter(node);
      }

      return range;
    };

    var isNodesInSameBlock = function (rootNode, node1, node2) {
      return getParentBlock(node1, rootNode) == getParentBlock(node2, rootNode);
    };

    var lean = function (left, rootNode, node) {
      var sibling, siblingName;

      if (left) {
        siblingName = 'previousSibling';
      } else {
        siblingName = 'nextSibling';
      }

      while (node && node != rootNode) {
        sibling = node[siblingName];

        if (isCaretContainer(sibling)) {
          sibling = sibling[siblingName];
        }

        if (isContentEditableFalse(sibling)) {
          if (isNodesInSameBlock(rootNode, sibling, node)) {
            return sibling;
          }

          break;
        }

        if (isCaretCandidate(sibling)) {
          break;
        }

        node = node.parentNode;
      }

      return null;
    };

    var before = curry(beforeAfter, true);
    var after = curry(beforeAfter, false);

    var normalizeRange = function (direction, rootNode, range) {
      var node, container, offset, location;
      var leanLeft = curry(lean, true, rootNode);
      var leanRight = curry(lean, false, rootNode);

      container = range.startContainer;
      offset = range.startOffset;

      if (CaretContainer.isCaretContainerBlock(container)) {
        if (!isElement(container)) {
          container = container.parentNode;
        }

        location = container.getAttribute('data-mce-caret');

        if (location == 'before') {
          node = container.nextSibling;
          if (isContentEditableFalse(node)) {
            return before(node);
          }
        }

        if (location == 'after') {
          node = container.previousSibling;
          if (isContentEditableFalse(node)) {
            return after(node);
          }
        }
      }

      if (!range.collapsed) {
        return range;
      }

      if (NodeType.isText(container)) {
        if (isCaretContainer(container)) {
          if (direction === 1) {
            node = leanRight(container);
            if (node) {
              return before(node);
            }

            node = leanLeft(container);
            if (node) {
              return after(node);
            }
          }

          if (direction === -1) {
            node = leanLeft(container);
            if (node) {
              return after(node);
            }

            node = leanRight(container);
            if (node) {
              return before(node);
            }
          }

          return range;
        }

        if (CaretContainer.endsWithCaretContainer(container) && offset >= container.data.length - 1) {
          if (direction === 1) {
            node = leanRight(container);
            if (node) {
              return before(node);
            }
          }

          return range;
        }

        if (CaretContainer.startsWithCaretContainer(container) && offset <= 1) {
          if (direction === -1) {
            node = leanLeft(container);
            if (node) {
              return after(node);
            }
          }

          return range;
        }

        if (offset === container.data.length) {
          node = leanRight(container);
          if (node) {
            return before(node);
          }

          return range;
        }

        if (offset === 0) {
          node = leanLeft(container);
          if (node) {
            return after(node);
          }

          return range;
        }
      }

      return range;
    };

    var isNextToContentEditableFalse = function (relativeOffset, caretPosition) {
      return isContentEditableFalse(getChildNodeAtRelativeOffset(relativeOffset, caretPosition));
    };

    return {
      isForwards: isForwards,
      isBackwards: isBackwards,
      findNode: findNode,
      getEditingHost: getEditingHost,
      getParentBlock: getParentBlock,
      isInSameBlock: isInSameBlock,
      isInSameEditingHost: isInSameEditingHost,
      isBeforeContentEditableFalse: curry(isNextToContentEditableFalse, 0),
      isAfterContentEditableFalse: curry(isNextToContentEditableFalse, -1),
      normalizeRange: normalizeRange
    };
  }
);

/**
 * CaretWalker.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
 */

/**
 * This module contains logic for moving around a virtual caret in logical order within a DOM element.
 *
 * It ignores the most obvious invalid caret locations such as within a script element or within a
 * contentEditable=false element but it will return locations that isn't possible to render visually.
 *
 * @private
 * @class tinymce.caret.CaretWalker
 * @example
 * var caretWalker = new CaretWalker(rootElm);
 *
 * var prevLogicalCaretPosition = caretWalker.prev(CaretPosition.fromRangeStart(range));
 * var nextLogicalCaretPosition = caretWalker.next(CaretPosition.fromRangeEnd(range));
 */
define(
  'tinymce.core.caret.CaretWalker',
  [
    "tinymce.core.dom.NodeType",
    "tinymce.core.caret.CaretCandidate",
    "tinymce.core.caret.CaretPosition",
    "tinymce.core.caret.CaretUtils",
    "tinymce.core.util.Arr",
    "tinymce.core.util.Fun"
  ],
  function (NodeType, CaretCandidate, CaretPosition, CaretUtils, Arr, Fun) {
    var isContentEditableFalse = NodeType.isContentEditableFalse,
      isText = NodeType.isText,
      isElement = NodeType.isElement,
      isBr = NodeType.isBr,
      isForwards = CaretUtils.isForwards,
      isBackwards = CaretUtils.isBackwards,
      isCaretCandidate = CaretCandidate.isCaretCandidate,
      isAtomic = CaretCandidate.isAtomic,
      isEditableCaretCandidate = CaretCandidate.isEditableCaretCandidate;

    var getParents = function (node, rootNode) {
      var parents = [];

      while (node && node != rootNode) {
        parents.push(node);
        node = node.parentNode;
      }

      return parents;
    };

    var nodeAtIndex = function (container, offset) {
      if (container.hasChildNodes() && offset < container.childNodes.length) {
        return container.childNodes[offset];
      }

      return null;
    };

    var getCaretCandidatePosition = function (direction, node) {
      if (isForwards(direction)) {
        if (isCaretCandidate(node.previousSibling) && !isText(node.previousSibling)) {
          return CaretPosition.before(node);
        }

        if (isText(node)) {
          return CaretPosition(node, 0);
        }
      }

      if (isBackwards(direction)) {
        if (isCaretCandidate(node.nextSibling) && !isText(node.nextSibling)) {
          return CaretPosition.after(node);
        }

        if (isText(node)) {
          return CaretPosition(node, node.data.length);
        }
      }

      if (isBackwards(direction)) {
        if (isBr(node)) {
          return CaretPosition.before(node);
        }

        return CaretPosition.after(node);
      }

      return CaretPosition.before(node);
    };

    // Jumps over BR elements <p>|<br></p><p>a</p> -> <p><br></p><p>|a</p>
    var isBrBeforeBlock = function (node, rootNode) {
      var next;

      if (!NodeType.isBr(node)) {
        return false;
      }

      next = findCaretPosition(1, CaretPosition.after(node), rootNode);
      if (!next) {
        return false;
      }

      return !CaretUtils.isInSameBlock(CaretPosition.before(node), CaretPosition.before(next), rootNode);
    };

    var findCaretPosition = function (direction, startCaretPosition, rootNode) {
      var container, offset, node, nextNode, innerNode,
        rootContentEditableFalseElm, caretPosition;

      if (!isElement(rootNode) || !startCaretPosition) {
        return null;
      }

      if (startCaretPosition.isEqual(CaretPosition.after(rootNode)) && rootNode.lastChild) {
        caretPosition = CaretPosition.after(rootNode.lastChild);
        if (isBackwards(direction) && isCaretCandidate(rootNode.lastChild) && isElement(rootNode.lastChild)) {
          return isBr(rootNode.lastChild) ? CaretPosition.before(rootNode.lastChild) : caretPosition;
        }
      } else {
        caretPosition = startCaretPosition;
      }

      container = caretPosition.container();
      offset = caretPosition.offset();

      if (isText(container)) {
        if (isBackwards(direction) && offset > 0) {
          return CaretPosition(container, --offset);
        }

        if (isForwards(direction) && offset < container.length) {
          return CaretPosition(container, ++offset);
        }

        node = container;
      } else {
        if (isBackwards(direction) && offset > 0) {
          nextNode = nodeAtIndex(container, offset - 1);
          if (isCaretCandidate(nextNode)) {
            if (!isAtomic(nextNode)) {
              innerNode = CaretUtils.findNode(nextNode, direction, isEditableCaretCandidate, nextNode);
              if (innerNode) {
                if (isText(innerNode)) {
                  return CaretPosition(innerNode, innerNode.data.length);
                }

                return CaretPosition.after(innerNode);
              }
            }

            if (isText(nextNode)) {
              return CaretPosition(nextNode, nextNode.data.length);
            }

            return CaretPosition.before(nextNode);
          }
        }

        if (isForwards(direction) && offset < container.childNodes.length) {
          nextNode = nodeAtIndex(container, offset);
          if (isCaretCandidate(nextNode)) {
            if (isBrBeforeBlock(nextNode, rootNode)) {
              return findCaretPosition(direction, CaretPosition.after(nextNode), rootNode);
            }

            if (!isAtomic(nextNode)) {
              innerNode = CaretUtils.findNode(nextNode, direction, isEditableCaretCandidate, nextNode);
              if (innerNode) {
                if (isText(innerNode)) {
                  return CaretPosition(innerNode, 0);
                }

                return CaretPosition.before(innerNode);
              }
            }

            if (isText(nextNode)) {
              return CaretPosition(nextNode, 0);
            }

            return CaretPosition.after(nextNode);
          }
        }

        node = caretPosition.getNode();
      }

      if ((isForwards(direction) && caretPosition.isAtEnd()) || (isBackwards(direction) && caretPosition.isAtStart())) {
        node = CaretUtils.findNode(node, direction, Fun.constant(true), rootNode, true);
        if (isEditableCaretCandidate(node)) {
          return getCaretCandidatePosition(direction, node);
        }
      }

      nextNode = CaretUtils.findNode(node, direction, isEditableCaretCandidate, rootNode);

      rootContentEditableFalseElm = Arr.last(Arr.filter(getParents(container, rootNode), isContentEditableFalse));
      if (rootContentEditableFalseElm && (!nextNode || !rootContentEditableFalseElm.contains(nextNode))) {
        if (isForwards(direction)) {
          caretPosition = CaretPosition.after(rootContentEditableFalseElm);
        } else {
          caretPosition = CaretPosition.before(rootContentEditableFalseElm);
        }

        return caretPosition;
      }

      if (nextNode) {
        return getCaretCandidatePosition(direction, nextNode);
      }

      return null;
    };

    return function (rootNode) {
      return {
        /**
         * Returns the next logical caret position from the specificed input
         * caretPoisiton or null if there isn't any more positions left for example
         * at the end specified root element.
         *
         * @method next
         * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from.
         * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found.
         */
        next: function (caretPosition) {
          return findCaretPosition(1, caretPosition, rootNode);
        },

        /**
         * Returns the previous logical caret position from the specificed input
         * caretPoisiton or null if there isn't any more positions left for example
         * at the end specified root element.
         *
         * @method prev
         * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from.
         * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found.
         */
        prev: function (caretPosition) {
          return findCaretPosition(-1, caretPosition, rootNode);
        }
      };
    };
  }
);
/**
 * CaretFinder.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
 */

define(
  'tinymce.core.caret.CaretFinder',
  [
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'tinymce.core.caret.CaretCandidate',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.caret.CaretUtils',
    'tinymce.core.caret.CaretWalker',
    'tinymce.core.dom.NodeType'
  ],
  function (Fun, Option, CaretCandidate, CaretPosition, CaretUtils, CaretWalker, NodeType) {
    var walkToPositionIn = function (forward, rootNode, startNode) {
      var position = forward ? CaretPosition.before(startNode) : CaretPosition.after(startNode);
      return fromPosition(forward, rootNode, position);
    };

    var afterElement = function (node) {
      return NodeType.isBr(node) ? CaretPosition.before(node) : CaretPosition.after(node);
    };

    var isBeforeOrStart = function (position) {
      if (CaretPosition.isTextPosition(position)) {
        return position.offset() === 0;
      } else {
        return CaretCandidate.isCaretCandidate(position.getNode());
      }
    };

    var isAfterOrEnd = function (position) {
      if (CaretPosition.isTextPosition(position)) {
        return position.offset() === position.container().data.length;
      } else {
        return CaretCandidate.isCaretCandidate(position.getNode(true));
      }
    };

    var isBeforeAfterSameElement = function (from, to) {
      return !CaretPosition.isTextPosition(from) && !CaretPosition.isTextPosition(to) && from.getNode() === to.getNode(true);
    };

    var isAtBr = function (position) {
      return !CaretPosition.isTextPosition(position) && NodeType.isBr(position.getNode());
    };

    var shouldSkipPosition = function (forward, from, to) {
      if (forward) {
        return !isBeforeAfterSameElement(from, to) && !isAtBr(from) && isAfterOrEnd(from) && isBeforeOrStart(to);
      } else {
        return !isBeforeAfterSameElement(to, from) && isBeforeOrStart(from) && isAfterOrEnd(to);
      }
    };

    // Finds: <p>a|<b>b</b></p> -> <p>a<b>|b</b></p>
    var fromPosition = function (forward, rootNode, position) {
      var walker = new CaretWalker(rootNode);
      return Option.from(forward ? walker.next(position) : walker.prev(position));
    };

    // Finds: <p>a|<b>b</b></p> -> <p>a<b>b|</b></p>
    var navigate = function (forward, rootNode, from) {
      return fromPosition(forward, rootNode, from).bind(function (to) {
        if (CaretUtils.isInSameBlock(from, to, rootNode) && shouldSkipPosition(forward, from, to)) {
          return fromPosition(forward, rootNode, to);
        } else {
          return Option.some(to);
        }
      });
    };

    var positionIn = function (forward, element) {
      var startNode = forward ? element.firstChild : element.lastChild;
      if (NodeType.isText(startNode)) {
        return Option.some(new CaretPosition(startNode, forward ? 0 : startNode.data.length));
      } else if (startNode) {
        if (CaretCandidate.isCaretCandidate(startNode)) {
          return Option.some(forward ? CaretPosition.before(startNode) : afterElement(startNode));
        } else {
          return walkToPositionIn(forward, element, startNode);
        }
      } else {
        return Option.none();
      }
    };

    return {
      fromPosition: fromPosition,
      nextPosition: Fun.curry(fromPosition, true),
      prevPosition: Fun.curry(fromPosition, false),
      navigate: navigate,
      positionIn: positionIn,
      firstPositionIn: Fun.curry(positionIn, true),
      lastPositionIn: Fun.curry(positionIn, false)
    };
  }
);

/**
 * RangeNormalizer.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
 */

define(
  'tinymce.core.dom.RangeNormalizer',
  [
    'global!document',
    'tinymce.core.caret.CaretFinder',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.caret.CaretUtils'
  ],
  function (document, CaretFinder, CaretPosition, CaretUtils) {
    var createRange = function (sc, so, ec, eo) {
      var rng = document.createRange();
      rng.setStart(sc, so);
      rng.setEnd(ec, eo);
      return rng;
    };

    // If you triple click a paragraph in this case:
    //   <blockquote><p>a</p></blockquote><p>b</p>
    // It would become this range in webkit:
    //   <blockquote><p>[a</p></blockquote><p>]b</p>
    // We would want it to be:
    //   <blockquote><p>[a]</p></blockquote><p>b</p>
    // Since it would otherwise produces spans out of thin air on insertContent for example.
    var normalizeBlockSelectionRange = function (rng) {
      var startPos = CaretPosition.fromRangeStart(rng);
      var endPos = CaretPosition.fromRangeEnd(rng);
      var rootNode = rng.commonAncestorContainer;

      return CaretFinder.fromPosition(false, rootNode, endPos)
        .map(function (newEndPos) {
          if (!CaretUtils.isInSameBlock(startPos, endPos, rootNode) && CaretUtils.isInSameBlock(startPos, newEndPos, rootNode)) {
            return createRange(startPos.container(), startPos.offset(), newEndPos.container(), newEndPos.offset());
          } else {
            return rng;
          }
        }).getOr(rng);
    };

    var normalizeBlockSelection = function (rng) {
      return rng.collapsed ? rng : normalizeBlockSelectionRange(rng);
    };

    var normalize = function (rng) {
      return normalizeBlockSelection(rng);
    };

    return {
      normalize: normalize
    };
  }
);
define(
  'ephox.katamari.api.Type',

  [
    'global!Array',
    'global!String'
  ],

  function (Array, String) {
    var typeOf = function(x) {
      if (x === null) return 'null';
      var t = typeof x;
      if (t === 'object' && Array.prototype.isPrototypeOf(x)) return 'array';
      if (t === 'object' && String.prototype.isPrototypeOf(x)) return 'string';
      return t;
    };

    var isType = function (type) {
      return function (value) {
        return typeOf(value) === type;
      };
    };

    return {
      isString: isType('string'),
      isObject: isType('object'),
      isArray: isType('array'),
      isNull: isType('null'),
      isBoolean: isType('boolean'),
      isUndefined: isType('undefined'),
      isFunction: isType('function'),
      isNumber: isType('number')
    };
  }
);


define(
  'ephox.katamari.data.Immutable',

  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Fun',
    'global!Array',
    'global!Error'
  ],

  function (Arr, Fun, Array, Error) {
    return function () {
      var fields = arguments;
      return function(/* values */) {
        //  Don't use array slice(arguments), makes the whole function unoptimisable on Chrome
        var values = new Array(arguments.length);
        for (var i = 0; i < values.length; i++) values[i] = arguments[i];

        if (fields.length !== values.length)
          throw new Error('Wrong number of arguments to struct. Expected "[' + fields.length + ']", got ' + values.length + ' arguments');

        var struct = {};
        Arr.each(fields, function (name, i) {
          struct[name] = Fun.constant(values[i]);
        });
        return struct;
      };
    };
  }
);

define(
  'ephox.katamari.api.Obj',

  [
    'ephox.katamari.api.Option',
    'global!Object'
  ],

  function (Option, Object) {
    // There are many variations of Object iteration that are faster than the 'for-in' style:
    // http://jsperf.com/object-keys-iteration/107
    //
    // Use the native keys if it is available (IE9+), otherwise fall back to manually filtering
    var keys = (function () {
      var fastKeys = Object.keys;

      // This technically means that 'each' and 'find' on IE8 iterate through the object twice.
      // This code doesn't run on IE8 much, so it's an acceptable tradeoff.
      // If it becomes a problem we can always duplicate the feature detection inside each and find as well.
      var slowKeys = function (o) {
        var r = [];
        for (var i in o) {
          if (o.hasOwnProperty(i)) {
            r.push(i);
          }
        }
        return r;
      };

      return fastKeys === undefined ? slowKeys : fastKeys;
    })();


    var each = function (obj, f) {
      var props = keys(obj);
      for (var k = 0, len = props.length; k < len; k++) {
        var i = props[k];
        var x = obj[i];
        f(x, i, obj);
      }
    };

    /** objectMap :: (JsObj(k, v), (v, k, JsObj(k, v) -> x)) -> JsObj(k, x) */
    var objectMap = function (obj, f) {
      return tupleMap(obj, function (x, i, obj) {
        return {
          k: i,
          v: f(x, i, obj)
        };
      });
    };

    /** tupleMap :: (JsObj(k, v), (v, k, JsObj(k, v) -> { k: x, v: y })) -> JsObj(x, y) */
    var tupleMap = function (obj, f) {
      var r = {};
      each(obj, function (x, i) {
        var tuple = f(x, i, obj);
        r[tuple.k] = tuple.v;
      });
      return r;
    };

    /** bifilter :: (JsObj(k, v), (v, k -> Bool)) -> { t: JsObj(k, v), f: JsObj(k, v) } */
    var bifilter = function (obj, pred) {
      var t = {};
      var f = {};
      each(obj, function(x, i) {
        var branch = pred(x, i) ? t : f;
        branch[i] = x;
      });
      return {
        t: t,
        f: f
      };
    };

    /** mapToArray :: (JsObj(k, v), (v, k -> a)) -> [a] */
    var mapToArray = function (obj, f) {
      var r = [];
      each(obj, function(value, name) {
        r.push(f(value, name));
      });
      return r;
    };

    /** find :: (JsObj(k, v), (v, k, JsObj(k, v) -> Bool)) -> Option v */
    var find = function (obj, pred) {
      var props = keys(obj);
      for (var k = 0, len = props.length; k < len; k++) {
        var i = props[k];
        var x = obj[i];
        if (pred(x, i, obj)) {
          return Option.some(x);
        }
      }
      return Option.none();
    };

    /** values :: JsObj(k, v) -> [v] */
    var values = function (obj) {
      return mapToArray(obj, function (v) {
        return v;
      });
    };

    var size = function (obj) {
      return values(obj).length;
    };

    return {
      bifilter: bifilter,
      each: each,
      map: objectMap,
      mapToArray: mapToArray,
      tupleMap: tupleMap,
      find: find,
      keys: keys,
      values: values,
      size: size
    };
  }
);
define(
  'ephox.katamari.util.BagUtils',

  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Type',
    'global!Error'
  ],

  function (Arr, Type, Error) {
    var sort = function (arr) {
      return arr.slice(0).sort();
    };

    var reqMessage = function (required, keys) {
      throw new Error('All required keys (' + sort(required).join(', ') + ') were not specified. Specified keys were: ' + sort(keys).join(', ') + '.');
    };

    var unsuppMessage = function (unsupported) {
      throw new Error('Unsupported keys for object: ' + sort(unsupported).join(', '));
    };

    var validateStrArr = function (label, array) {
      if (!Type.isArray(array)) throw new Error('The ' + label + ' fields must be an array. Was: ' + array + '.');
      Arr.each(array, function (a) {
        if (!Type.isString(a)) throw new Error('The value ' + a + ' in the ' + label + ' fields was not a string.');
      });
    };

    var invalidTypeMessage = function (incorrect, type) {
      throw new Error('All values need to be of type: ' + type + '. Keys (' + sort(incorrect).join(', ') + ') were not.');
    };

    var checkDupes = function (everything) {
      var sorted = sort(everything);
      var dupe = Arr.find(sorted, function (s, i) {
        return i < sorted.length -1 && s === sorted[i + 1];
      });

      dupe.each(function (d) {
        throw new Error('The field: ' + d + ' occurs more than once in the combined fields: [' + sorted.join(', ') + '].');
      });
    };

    return {
      sort: sort,
      reqMessage: reqMessage,
      unsuppMessage: unsuppMessage,
      validateStrArr: validateStrArr,
      invalidTypeMessage: invalidTypeMessage,
      checkDupes: checkDupes
    };
  }
);
define(
  'ephox.katamari.data.MixedBag',

  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Obj',
    'ephox.katamari.api.Option',
    'ephox.katamari.util.BagUtils',
    'global!Error',
    'global!Object'
  ],

  function (Arr, Fun, Obj, Option, BagUtils, Error, Object) {
    
    return function (required, optional) {
      var everything = required.concat(optional);
      if (everything.length === 0) throw new Error('You must specify at least one required or optional field.');

      BagUtils.validateStrArr('required', required);
      BagUtils.validateStrArr('optional', optional);

      BagUtils.checkDupes(everything);

      return function (obj) {
        var keys = Obj.keys(obj);

        // Ensure all required keys are present.
        var allReqd = Arr.forall(required, function (req) {
          return Arr.contains(keys, req);
        });

        if (! allReqd) BagUtils.reqMessage(required, keys);

        var unsupported = Arr.filter(keys, function (key) {
          return !Arr.contains(everything, key);
        });

        if (unsupported.length > 0) BagUtils.unsuppMessage(unsupported);

        var r = {};
        Arr.each(required, function (req) {
          r[req] = Fun.constant(obj[req]);
        });

        Arr.each(optional, function (opt) {
          r[opt] = Fun.constant(Object.prototype.hasOwnProperty.call(obj, opt) ? Option.some(obj[opt]): Option.none());
        });

        return r;
      };
    };
  }
);
define(
  'ephox.katamari.api.Struct',

  [
    'ephox.katamari.data.Immutable',
    'ephox.katamari.data.MixedBag'
  ],

  function (Immutable, MixedBag) {
    return {
      immutable: Immutable,
      immutableBag: MixedBag
    };
  }
);

define(
  'ephox.sugar.alien.Recurse',

  [

  ],

  function () {
    /**
     * Applies f repeatedly until it completes (by returning Option.none()).
     *
     * Normally would just use recursion, but JavaScript lacks tail call optimisation.
     *
     * This is what recursion looks like when manually unravelled :)
     */
    var toArray = function (target, f) {
      var r = [];

      var recurse = function (e) {
        r.push(e);
        return f(e);
      };

      var cur = f(target);
      do {
        cur = cur.bind(recurse);
      } while (cur.isSome());

      return r;
    };

    return {
      toArray: toArray
    };
  }
);
define(
  'ephox.sand.api.Node',

  [
    'ephox.sand.util.Global'
  ],

  function (Global) {
    /*
     * MDN says (yes) for IE, but it's undefined on IE8
     */
    var node = function () {
      var f = Global.getOrDie('Node');
      return f;
    };

    /*
     * Most of numerosity doesn't alter the methods on the object.
     * We're making an exception for Node, because bitwise and is so easy to get wrong.
     *
     * Might be nice to ADT this at some point instead of having individual methods.
     */

    var compareDocumentPosition = function (a, b, match) {
      // Returns: 0 if e1 and e2 are the same node, or a bitmask comparing the positions
      // of nodes e1 and e2 in their documents. See the URL below for bitmask interpretation
      // https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition
      return (a.compareDocumentPosition(b) & match) !== 0;
    };

    var documentPositionPreceding = function (a, b) {
      return compareDocumentPosition(a, b, node().DOCUMENT_POSITION_PRECEDING);
    };

    var documentPositionContainedBy = function (a, b) {
      return compareDocumentPosition(a, b, node().DOCUMENT_POSITION_CONTAINED_BY);
    };

    return {
      documentPositionPreceding: documentPositionPreceding,
      documentPositionContainedBy: documentPositionContainedBy
    };
  }
);
define(
  'ephox.katamari.api.Thunk',

  [
  ],

  function () {

    var cached = function (f) {
      var called = false;
      var r;
      return function() {
        if (!called) {
          called = true;
          r = f.apply(null, arguments);
        }
        return r;
      };
    };

    return {
      cached: cached
    };
  }
);

defineGlobal("global!Number", Number);
define(
  'ephox.sand.detect.Version',

  [
    'ephox.katamari.api.Arr',
    'global!Number',
    'global!String'
  ],

  function (Arr, Number, String) {
    var firstMatch = function (regexes, s) {
      for (var i = 0; i < regexes.length; i++) {
        var x = regexes[i];
        if (x.test(s)) return x;
      }
      return undefined;
    };

    var find = function (regexes, agent) {
      var r = firstMatch(regexes, agent);
      if (!r) return { major : 0, minor : 0 };
      var group = function(i) {
        return Number(agent.replace(r, '$' + i));
      };
      return nu(group(1), group(2));
    };

    var detect = function (versionRegexes, agent) {
      var cleanedAgent = String(agent).toLowerCase();

      if (versionRegexes.length === 0) return unknown();
      return find(versionRegexes, cleanedAgent);
    };

    var unknown = function () {
      return nu(0, 0);
    };

    var nu = function (major, minor) {
      return { major: major, minor: minor };
    };

    return {
      nu: nu,
      detect: detect,
      unknown: unknown
    };
  }
);
define(
  'ephox.sand.core.Browser',

  [
    'ephox.katamari.api.Fun',
    'ephox.sand.detect.Version'
  ],

  function (Fun, Version) {
    var edge = 'Edge';
    var chrome = 'Chrome';
    var ie = 'IE';
    var opera = 'Opera';
    var firefox = 'Firefox';
    var safari = 'Safari';

    var isBrowser = function (name, current) {
      return function () {
        return current === name;
      };
    };

    var unknown = function () {
      return nu({
        current: undefined,
        version: Version.unknown()
      });
    };

    var nu = function (info) {
      var current = info.current;
      var version = info.version;

      return {
        current: current,
        version: version,

        // INVESTIGATE: Rename to Edge ?
        isEdge: isBrowser(edge, current),
        isChrome: isBrowser(chrome, current),
        // NOTE: isIe just looks too weird
        isIE: isBrowser(ie, current),
        isOpera: isBrowser(opera, current),
        isFirefox: isBrowser(firefox, current),
        isSafari: isBrowser(safari, current)
      };
    };

    return {
      unknown: unknown,
      nu: nu,
      edge: Fun.constant(edge),
      chrome: Fun.constant(chrome),
      ie: Fun.constant(ie),
      opera: Fun.constant(opera),
      firefox: Fun.constant(firefox),
      safari: Fun.constant(safari)
    };
  }
);
define(
  'ephox.sand.core.OperatingSystem',

  [
    'ephox.katamari.api.Fun',
    'ephox.sand.detect.Version'
  ],

  function (Fun, Version) {
    var windows = 'Windows';
    var ios = 'iOS';
    var android = 'Android';
    var linux = 'Linux';
    var osx = 'OSX';
    var solaris = 'Solaris';
    var freebsd = 'FreeBSD';

    // Though there is a bit of dupe with this and Browser, trying to 
    // reuse code makes it much harder to follow and change.
    var isOS = function (name, current) {
      return function () {
        return current === name;
      };
    };

    var unknown = function () {
      return nu({
        current: undefined,
        version: Version.unknown()
      });
    };

    var nu = function (info) {
      var current = info.current;
      var version = info.version;

      return {
        current: current,
        version: version,

        isWindows: isOS(windows, current),
        // TODO: Fix capitalisation
        isiOS: isOS(ios, current),
        isAndroid: isOS(android, current),
        isOSX: isOS(osx, current),
        isLinux: isOS(linux, current),
        isSolaris: isOS(solaris, current),
        isFreeBSD: isOS(freebsd, current)
      };
    };

    return {
      unknown: unknown,
      nu: nu,

      windows: Fun.constant(windows),
      ios: Fun.constant(ios),
      android: Fun.constant(android),
      linux: Fun.constant(linux),
      osx: Fun.constant(osx),
      solaris: Fun.constant(solaris),
      freebsd: Fun.constant(freebsd)
    };
  }
);
define(
  'ephox.sand.detect.DeviceType',

  [
    'ephox.katamari.api.Fun'
  ],

  function (Fun) {
    return function (os, browser, userAgent) {
      var isiPad = os.isiOS() && /ipad/i.test(userAgent) === true;
      var isiPhone = os.isiOS() && !isiPad;
      var isAndroid3 = os.isAndroid() && os.version.major === 3;
      var isAndroid4 = os.isAndroid() && os.version.major === 4;
      var isTablet = isiPad || isAndroid3 || ( isAndroid4 && /mobile/i.test(userAgent) === true );
      var isTouch = os.isiOS() || os.isAndroid();
      var isPhone = isTouch && !isTablet;

      var iOSwebview = browser.isSafari() && os.isiOS() && /safari/i.test(userAgent) === false;

      return {
        isiPad : Fun.constant(isiPad),
        isiPhone: Fun.constant(isiPhone),
        isTablet: Fun.constant(isTablet),
        isPhone: Fun.constant(isPhone),
        isTouch: Fun.constant(isTouch),
        isAndroid: os.isAndroid,
        isiOS: os.isiOS,
        isWebView: Fun.constant(iOSwebview)
      };
    };
  }
);
define(
  'ephox.sand.detect.UaString',

  [
    'ephox.katamari.api.Arr',
    'ephox.sand.detect.Version',
    'global!String'
  ],

  function (Arr, Version, String) {
    var detect = function (candidates, userAgent) {
      var agent = String(userAgent).toLowerCase();
      return Arr.find(candidates, function (candidate) {
        return candidate.search(agent);
      });
    };

    // They (browser and os) are the same at the moment, but they might
    // not stay that way.
    var detectBrowser = function (browsers, userAgent) {
      return detect(browsers, userAgent).map(function (browser) {
        var version = Version.detect(browser.versionRegexes, userAgent);
        return {
          current: browser.name,
          version: version
        };
      });
    };

    var detectOs = function (oses, userAgent) {
      return detect(oses, userAgent).map(function (os) {
        var version = Version.detect(os.versionRegexes, userAgent);
        return {
          current: os.name,
          version: version
        };
      });
    };

    return {
      detectBrowser: detectBrowser,
      detectOs: detectOs
    };
  }
);
define(
  'ephox.katamari.str.StrAppend',

  [

  ],

  function () {
    var addToStart = function (str, prefix) {
      return prefix + str;
    };

    var addToEnd = function (str, suffix) {
      return str + suffix;
    };

    var removeFromStart = function (str, numChars) {
      return str.substring(numChars);
    };

    var removeFromEnd = function (str, numChars) {
      return str.substring(0, str.length - numChars);
    };
 
    return {
      addToStart: addToStart,
      addToEnd: addToEnd,
      removeFromStart: removeFromStart,
      removeFromEnd: removeFromEnd
    };
  }
);
define(
  'ephox.katamari.str.StringParts',

  [
    'ephox.katamari.api.Option',
    'global!Error'
  ],

  function (Option, Error) {
    /** Return the first 'count' letters from 'str'.
-     *  e.g. first("abcde", 2) === "ab"
-     */
    var first = function(str, count) {
     return str.substr(0, count);
    };

    /** Return the last 'count' letters from 'str'.
    *  e.g. last("abcde", 2) === "de"
    */
    var last = function(str, count) {
     return str.substr(str.length - count, str.length);
    };

    var head = function(str) {
      return str === '' ? Option.none() : Option.some(str.substr(0, 1));
    };

    var tail = function(str) {
      return str === '' ? Option.none() : Option.some(str.substring(1));
    };

    return {
      first: first,
      last: last,
      head: head,
      tail: tail
    };
  }
);
define(
  'ephox.katamari.api.Strings',

  [
    'ephox.katamari.str.StrAppend',
    'ephox.katamari.str.StringParts',
    'global!Error'
  ],

  function (StrAppend, StringParts, Error) {
    var checkRange = function(str, substr, start) {
      if (substr === '') return true;
      if (str.length < substr.length) return false;
      var x = str.substr(start, start + substr.length);
      return x === substr;
    };

    /** Given a string and object, perform template-replacements on the string, as specified by the object.
     * Any template fields of the form ${name} are replaced by the string or number specified as obj["name"]
     * Based on Douglas Crockford's 'supplant' method for template-replace of strings. Uses different template format.
     */
    var supplant = function(str, obj) {
      var isStringOrNumber = function(a) {
        var t = typeof a;
        return t === 'string' || t === 'number';
      };

      return str.replace(/\${([^{}]*)}/g,
        function (a, b) {
          var value = obj[b];
          return isStringOrNumber(value) ? value : a;
        }
      );
    };

    var removeLeading = function (str, prefix) {
      return startsWith(str, prefix) ? StrAppend.removeFromStart(str, prefix.length) : str;
    };

    var removeTrailing = function (str, prefix) {
      return endsWith(str, prefix) ? StrAppend.removeFromEnd(str, prefix.length) : str;
    };

    var ensureLeading = function (str, prefix) {
      return startsWith(str, prefix) ? str : StrAppend.addToStart(str, prefix);
    };

    var ensureTrailing = function (str, prefix) {
      return endsWith(str, prefix) ? str : StrAppend.addToEnd(str, prefix);
    };
 
    var contains = function(str, substr) {
      return str.indexOf(substr) !== -1;
    };

    var capitalize = function(str) {
      return StringParts.head(str).bind(function (head) {
        return StringParts.tail(str).map(function (tail) {
          return head.toUpperCase() + tail;
        });
      }).getOr(str);
    };

    /** Does 'str' start with 'prefix'?
     *  Note: all strings start with the empty string.
     *        More formally, for all strings x, startsWith(x, "").
     *        This is so that for all strings x and y, startsWith(y + x, y)
     */
    var startsWith = function(str, prefix) {
      return checkRange(str, prefix, 0);
    };

    /** Does 'str' end with 'suffix'?
     *  Note: all strings end with the empty string.
     *        More formally, for all strings x, endsWith(x, "").
     *        This is so that for all strings x and y, endsWith(x + y, y)
     */
    var endsWith = function(str, suffix) {
      return checkRange(str, suffix, str.length - suffix.length);
    };

   
    /** removes all leading and trailing spaces */
    var trim = function(str) {
      return str.replace(/^\s+|\s+$/g, '');
    };

    var lTrim = function(str) {
      return str.replace(/^\s+/g, '');
    };

    var rTrim = function(str) {
      return str.replace(/\s+$/g, '');
    };

    return {
      supplant: supplant,
      startsWith: startsWith,
      removeLeading: removeLeading,
      removeTrailing: removeTrailing,
      ensureLeading: ensureLeading,
      ensureTrailing: ensureTrailing,
      endsWith: endsWith,
      contains: contains,
      trim: trim,
      lTrim: lTrim,
      rTrim: rTrim,
      capitalize: capitalize
    };
  }
);

define(
  'ephox.sand.info.PlatformInfo',

  [
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Strings'
  ],

  function (Fun, Strings) {
    var normalVersionRegex = /.*?version\/\ ?([0-9]+)\.([0-9]+).*/;

    var checkContains = function (target) {
      return function (uastring) {
        return Strings.contains(uastring, target);
      };
    };

    var browsers = [
      {
        name : 'Edge',
        versionRegexes: [/.*?edge\/ ?([0-9]+)\.([0-9]+)$/],
        search: function (uastring) {
          var monstrosity = Strings.contains(uastring, 'edge/') && Strings.contains(uastring, 'chrome') && Strings.contains(uastring, 'safari') && Strings.contains(uastring, 'applewebkit');
          return monstrosity;
        }
      },
      {
        name : 'Chrome',
        versionRegexes: [/.*?chrome\/([0-9]+)\.([0-9]+).*/, normalVersionRegex],
        search : function (uastring) {
          return Strings.contains(uastring, 'chrome') && !Strings.contains(uastring, 'chromeframe');
        }
      },
      {
        name : 'IE',
        versionRegexes: [/.*?msie\ ?([0-9]+)\.([0-9]+).*/, /.*?rv:([0-9]+)\.([0-9]+).*/],
        search: function (uastring) {
          return Strings.contains(uastring, 'msie') || Strings.contains(uastring, 'trident');
        }
      },
      // INVESTIGATE: Is this still the Opera user agent?
      {
        name : 'Opera',
        versionRegexes: [normalVersionRegex, /.*?opera\/([0-9]+)\.([0-9]+).*/],
        search : checkContains('opera')
      },
      {
        name : 'Firefox',
        versionRegexes: [/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/],
        search : checkContains('firefox')
      },
      {
        name : 'Safari',
        versionRegexes: [normalVersionRegex, /.*?cpu os ([0-9]+)_([0-9]+).*/],
        search : function (uastring) {
          return (Strings.contains(uastring, 'safari') || Strings.contains(uastring, 'mobile/')) && Strings.contains(uastring, 'applewebkit');
        }
      }
    ];

    var oses = [
      {
        name : 'Windows',
        search : checkContains('win'),
        versionRegexes: [/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/]
      },
      {
        name : 'iOS',
        search : function (uastring) {
          return Strings.contains(uastring, 'iphone') || Strings.contains(uastring, 'ipad');
        },
        versionRegexes: [/.*?version\/\ ?([0-9]+)\.([0-9]+).*/, /.*cpu os ([0-9]+)_([0-9]+).*/, /.*cpu iphone os ([0-9]+)_([0-9]+).*/]
      },
      {
        name : 'Android',
        search : checkContains('android'),
        versionRegexes: [/.*?android\ ?([0-9]+)\.([0-9]+).*/]
      },
      {
        name : 'OSX',
        search : checkContains('os x'),
        versionRegexes: [/.*?os\ x\ ?([0-9]+)_([0-9]+).*/]
      },
      {
        name : 'Linux',
        search : checkContains('linux'),
        versionRegexes: [ ]
      },
      { name : 'Solaris',
        search : checkContains('sunos'),
        versionRegexes: [ ]
      },
      {
       name : 'FreeBSD',
       search : checkContains('freebsd'),
       versionRegexes: [ ]
      }
    ];

    return {
      browsers: Fun.constant(browsers),
      oses: Fun.constant(oses)
    };
  }
);
define(
  'ephox.sand.core.PlatformDetection',

  [
    'ephox.sand.core.Browser',
    'ephox.sand.core.OperatingSystem',
    'ephox.sand.detect.DeviceType',
    'ephox.sand.detect.UaString',
    'ephox.sand.info.PlatformInfo'
  ],

  function (Browser, OperatingSystem, DeviceType, UaString, PlatformInfo) {
    var detect = function (userAgent) {
      var browsers = PlatformInfo.browsers();
      var oses = PlatformInfo.oses();

      var browser = UaString.detectBrowser(browsers, userAgent).fold(
        Browser.unknown,
        Browser.nu
      );
      var os = UaString.detectOs(oses, userAgent).fold(
        OperatingSystem.unknown,
        OperatingSystem.nu
      );
      var deviceType = DeviceType(os, browser, userAgent);

      return {
        browser: browser,
        os: os,
        deviceType: deviceType
      };
    };

    return {
      detect: detect
    };
  }
);
define(
  'ephox.sand.api.PlatformDetection',

  [
    'ephox.katamari.api.Thunk',
    'ephox.sand.core.PlatformDetection',
    'global!navigator'
  ],

  function (Thunk, PlatformDetection, navigator) {
    var detect = Thunk.cached(function () {
      var userAgent = navigator.userAgent;
      return PlatformDetection.detect(userAgent);
    });

    return {
      detect: detect
    };
  }
);
define(
  'ephox.sugar.api.node.NodeTypes',

  [

  ],

  function () {
    return {
      ATTRIBUTE:              2,
      CDATA_SECTION:          4,
      COMMENT:                8,
      DOCUMENT:               9,
      DOCUMENT_TYPE:          10,
      DOCUMENT_FRAGMENT:      11,
      ELEMENT:                1,
      TEXT:                   3,
      PROCESSING_INSTRUCTION: 7,
      ENTITY_REFERENCE:       5,
      ENTITY:                 6,
      NOTATION:               12
    };
  }
);
define(
  'ephox.sugar.api.search.Selectors',

  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Option',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.node.NodeTypes',
    'global!Error',
    'global!document'
  ],

  function (Arr, Option, Element, NodeTypes, Error, document) {
    /*
     * There's a lot of code here; the aim is to allow the browser to optimise constant comparisons,
     * instead of doing object lookup feature detection on every call
     */
    var STANDARD = 0;
    var MSSTANDARD = 1;
    var WEBKITSTANDARD = 2;
    var FIREFOXSTANDARD = 3;

    var selectorType = (function () {
      var test = document.createElement('span');
      // As of Chrome 34 / Safari 7.1 / FireFox 34, everyone except IE has the unprefixed function.
      // Still check for the others, but do it last.
      return test.matches !== undefined ? STANDARD :
             test.msMatchesSelector !== undefined ? MSSTANDARD :
             test.webkitMatchesSelector !== undefined ? WEBKITSTANDARD :
             test.mozMatchesSelector !== undefined ? FIREFOXSTANDARD :
             -1;
    })();


    var ELEMENT = NodeTypes.ELEMENT;
    var DOCUMENT = NodeTypes.DOCUMENT;

    var is = function (element, selector) {
      var elem = element.dom();
      if (elem.nodeType !== ELEMENT) return false; // documents have querySelector but not matches

      // As of Chrome 34 / Safari 7.1 / FireFox 34, everyone except IE has the unprefixed function.
      // Still check for the others, but do it last.
      else if (selectorType === STANDARD) return elem.matches(selector);
      else if (selectorType === MSSTANDARD) return elem.msMatchesSelector(selector);
      else if (selectorType === WEBKITSTANDARD) return elem.webkitMatchesSelector(selector);
      else if (selectorType === FIREFOXSTANDARD) return elem.mozMatchesSelector(selector);
      else throw new Error('Browser lacks native selectors'); // unfortunately we can't throw this on startup :(
    };

    var bypassSelector = function (dom) {
      // Only elements and documents support querySelector
      return dom.nodeType !== ELEMENT && dom.nodeType !== DOCUMENT ||
              // IE fix for complex queries on empty nodes: http://jsfiddle.net/spyder/fv9ptr5L/
              dom.childElementCount === 0;
    };

    var all = function (selector, scope) {
      var base = scope === undefined ? document : scope.dom();
      return bypassSelector(base) ? [] : Arr.map(base.querySelectorAll(selector), Element.fromDom);
    };

    var one = function (selector, scope) {
      var base = scope === undefined ? document : scope.dom();
      return bypassSelector(base) ? Option.none() : Option.from(base.querySelector(selector)).map(Element.fromDom);
    };

    return {
      all: all,
      is: is,
      one: one
    };
  }
);

define(
  'ephox.sugar.api.dom.Compare',

  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Fun',
    'ephox.sand.api.Node',
    'ephox.sand.api.PlatformDetection',
    'ephox.sugar.api.search.Selectors'
  ],

  function (Arr, Fun, Node, PlatformDetection, Selectors) {

    var eq = function (e1, e2) {
      return e1.dom() === e2.dom();
    };

    var isEqualNode = function (e1, e2) {
      return e1.dom().isEqualNode(e2.dom());
    };

    var member = function (element, elements) {
      return Arr.exists(elements, Fun.curry(eq, element));
    };

    // DOM contains() method returns true if e1===e2, we define our contains() to return false (a node does not contain itself).
    var regularContains = function (e1, e2) {
      var d1 = e1.dom(), d2 = e2.dom();
      return d1 === d2 ? false : d1.contains(d2);
    };

    var ieContains = function (e1, e2) {
      // IE only implements the contains() method for Element nodes.
      // It fails for Text nodes, so implement it using compareDocumentPosition()
      // https://connect.microsoft.com/IE/feedback/details/780874/node-contains-is-incorrect
      // Note that compareDocumentPosition returns CONTAINED_BY if 'e2 *is_contained_by* e1':
      // Also, compareDocumentPosition defines a node containing itself as false.
      return Node.documentPositionContainedBy(e1.dom(), e2.dom());
    };

    var browser = PlatformDetection.detect().browser;

    // Returns: true if node e1 contains e2, otherwise false.
    // (returns false if e1===e2: A node does not contain itself).
    var contains = browser.isIE() ? ieContains : regularContains;

    return {
      eq: eq,
      isEqualNode: isEqualNode,
      member: member,
      contains: contains,

      // Only used by DomUniverse. Remove (or should Selectors.is move here?)
      is: Selectors.is
    };
  }
);

define(
  'ephox.sugar.api.search.Traverse',

  [
    'ephox.katamari.api.Type',
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'ephox.katamari.api.Struct',
    'ephox.sugar.alien.Recurse',
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.node.Element'
  ],

  function (Type, Arr, Fun, Option, Struct, Recurse, Compare, Element) {
    // The document associated with the current element
    var owner = function (element) {
      return Element.fromDom(element.dom().ownerDocument);
    };

    var documentElement = function (element) {
      // TODO: Avoid unnecessary wrap/unwrap here
      var doc = owner(element);
      return Element.fromDom(doc.dom().documentElement);
    };

    // The window element associated with the element
    var defaultView = function (element) {
      var el = element.dom();
      var defaultView = el.ownerDocument.defaultView;
      return Element.fromDom(defaultView);
    };

    var parent = function (element) {
      var dom = element.dom();
      return Option.from(dom.parentNode).map(Element.fromDom);
    };

    var findIndex = function (element) {
      return parent(element).bind(function (p) {
        // TODO: Refactor out children so we can avoid the constant unwrapping
        var kin = children(p);
        return Arr.findIndex(kin, function (elem) {
          return Compare.eq(element, elem);
        });
      });
    };

    var parents = function (element, isRoot) {
      var stop = Type.isFunction(isRoot) ? isRoot : Fun.constant(false);

      // This is used a *lot* so it needs to be performant, not recursive
      var dom = element.dom();
      var ret = [];

      while (dom.parentNode !== null && dom.parentNode !== undefined) {
        var rawParent = dom.parentNode;
        var parent = Element.fromDom(rawParent);
        ret.push(parent);

        if (stop(parent) === true) break;
        else dom = rawParent;
      }
      return ret;
    };

    var siblings = function (element) {
      // TODO: Refactor out children so we can just not add self instead of filtering afterwards
      var filterSelf = function (elements) {
        return Arr.filter(elements, function (x) {
          return !Compare.eq(element, x);
        });
      };

      return parent(element).map(children).map(filterSelf).getOr([]);
    };

    var offsetParent = function (element) {
      var dom = element.dom();
      return Option.from(dom.offsetParent).map(Element.fromDom);
    };

    var prevSibling = function (element) {
      var dom = element.dom();
      return Option.from(dom.previousSibling).map(Element.fromDom);
    };

    var nextSibling = function (element) {
      var dom = element.dom();
      return Option.from(dom.nextSibling).map(Element.fromDom);
    };

    var prevSiblings = function (element) {
      // This one needs to be reversed, so they're still in DOM order
      return Arr.reverse(Recurse.toArray(element, prevSibling));
    };

    var nextSiblings = function (element) {
      return Recurse.toArray(element, nextSibling);
    };

    var children = function (element) {
      var dom = element.dom();
      return Arr.map(dom.childNodes, Element.fromDom);
    };

    var child = function (element, index) {
      var children = element.dom().childNodes;
      return Option.from(children[index]).map(Element.fromDom);
    };

    var firstChild = function (element) {
      return child(element, 0);
    };

    var lastChild = function (element) {
      return child(element, element.dom().childNodes.length - 1);
    };

    var childNodesCount = function (element, index) {
      return element.dom().childNodes.length;
    };

    var spot = Struct.immutable('element', 'offset');
    var leaf = function (element, offset) {
      var cs = children(element);
      return cs.length > 0 && offset < cs.length ? spot(cs[offset], 0) : spot(element, offset);
    };

    return {
      owner: owner,
      defaultView: defaultView,
      documentElement: documentElement,
      parent: parent,
      findIndex: findIndex,
      parents: parents,
      siblings: siblings,
      prevSibling: prevSibling,
      offsetParent: offsetParent,
      prevSiblings: prevSiblings,
      nextSibling: nextSibling,
      nextSiblings: nextSiblings,
      children: children,
      child: child,
      firstChild: firstChild,
      lastChild: lastChild,
      childNodesCount: childNodesCount,
      leaf: leaf
    };
  }
);

define(
  'ephox.sugar.api.dom.Insert',

  [
    'ephox.sugar.api.search.Traverse'
  ],

  function (Traverse) {
    var before = function (marker, element) {
      var parent = Traverse.parent(marker);
      parent.each(function (v) {
        v.dom().insertBefore(element.dom(), marker.dom());
      });
    };

    var after = function (marker, element) {
      var sibling = Traverse.nextSibling(marker);
      sibling.fold(function () {
        var parent = Traverse.parent(marker);
        parent.each(function (v) {
          append(v, element);
        });
      }, function (v) {
        before(v, element);
      });
    };

    var prepend = function (parent, element) {
      var firstChild = Traverse.firstChild(parent);
      firstChild.fold(function () {
        append(parent, element);
      }, function (v) {
        parent.dom().insertBefore(element.dom(), v.dom());
      });
    };

    var append = function (parent, element) {
      parent.dom().appendChild(element.dom());
    };

    var appendAt = function (parent, element, index) {
      Traverse.child(parent, index).fold(function () {
        append(parent, element);
      }, function (v) {
        before(v, element);
      });
    };

    var wrap = function (element, wrapper) {
      before(element, wrapper);
      append(wrapper, element);
    };

    return {
      before: before,
      after: after,
      prepend: prepend,
      append: append,
      appendAt: appendAt,
      wrap: wrap
    };
  }
);

define(
  'ephox.sugar.api.dom.InsertAll',

  [
    'ephox.katamari.api.Arr',
    'ephox.sugar.api.dom.Insert'
  ],

  function (Arr, Insert) {
    var before = function (marker, elements) {
      Arr.each(elements, function (x) {
        Insert.before(marker, x);
      });
    };

    var after = function (marker, elements) {
      Arr.each(elements, function (x, i) {
        var e = i === 0 ? marker : elements[i - 1];
        Insert.after(e, x);
      });
    };

    var prepend = function (parent, elements) {
      Arr.each(elements.slice().reverse(), function (x) {
        Insert.prepend(parent, x);
      });
    };

    var append = function (parent, elements) {
      Arr.each(elements, function (x) {
        Insert.append(parent, x);
      });
    };

    return {
      before: before,
      after: after,
      prepend: prepend,
      append: append
    };
  }
);

define(
  'ephox.sugar.api.dom.Remove',

  [
    'ephox.katamari.api.Arr',
    'ephox.sugar.api.dom.InsertAll',
    'ephox.sugar.api.search.Traverse'
  ],

  function (Arr, InsertAll, Traverse) {
    var empty = function (element) {
      // shortcut "empty node" trick. Requires IE 9.
      element.dom().textContent = '';

      // If the contents was a single empty text node, the above doesn't remove it. But, it's still faster in general
      // than removing every child node manually.
      // The following is (probably) safe for performance as 99.9% of the time the trick works and
      // Traverse.children will return an empty array.
      Arr.each(Traverse.children(element), function (rogue) {
        remove(rogue);
      });
    };

    var remove = function (element) {
      var dom = element.dom();
      if (dom.parentNode !== null)
        dom.parentNode.removeChild(dom);
    };

    var unwrap = function (wrapper) {
      var children = Traverse.children(wrapper);
      if (children.length > 0)
        InsertAll.before(wrapper, children);
      remove(wrapper);
    };

    return {
      empty: empty,
      remove: remove,
      unwrap: unwrap
    };
  }
);

define(
  'ephox.sugar.api.node.Node',

  [
    'ephox.sugar.api.node.NodeTypes'
  ],

  function (NodeTypes) {
    var name = function (element) {
      var r = element.dom().nodeName;
      return r.toLowerCase();
    };

    var type = function (element) {
      return element.dom().nodeType;
    };

    var value = function (element) {
      return element.dom().nodeValue;
    };

    var isType = function (t) {
      return function (element) {
        return type(element) === t;
      };
    };

    var isComment = function (element) {
      return type(element) === NodeTypes.COMMENT || name(element) === '#comment';
    };

    var isElement = isType(NodeTypes.ELEMENT);
    var isText = isType(NodeTypes.TEXT);
    var isDocument = isType(NodeTypes.DOCUMENT);

    return {
      name: name,
      type: type,
      value: value,
      isElement: isElement,
      isText: isText,
      isDocument: isDocument,
      isComment: isComment
    };
  }
);

define(
  'ephox.sugar.impl.NodeValue',

  [
    'ephox.sand.api.PlatformDetection',
    'ephox.katamari.api.Option',
    'global!Error'
  ],

  function (PlatformDetection, Option, Error) {
    return function (is, name) {
      var get = function (element) {
        if (!is(element)) throw new Error('Can only get ' + name + ' value of a ' + name + ' node');
        return getOption(element).getOr('');
      };

      var getOptionIE10 = function (element) {
        // Prevent IE10 from throwing exception when setting parent innerHTML clobbers (TBIO-451).
        try {
          return getOptionSafe(element);
        } catch (e) {
          return Option.none();
        }
      };

      var getOptionSafe = function (element) {
        return is(element) ? Option.from(element.dom().nodeValue) : Option.none();
      };

      var browser = PlatformDetection.detect().browser;
      var getOption = browser.isIE() && browser.version.major === 10 ? getOptionIE10 : getOptionSafe;

      var set = function (element, value) {
        if (!is(element)) throw new Error('Can only set raw ' + name + ' value of a ' + name + ' node');
        element.dom().nodeValue = value;
      };

      return {
        get: get,
        getOption: getOption,
        set: set
      };
    };
  }
);
define(
  'ephox.sugar.api.node.Text',

  [
    'ephox.sugar.api.node.Node',
    'ephox.sugar.impl.NodeValue'
  ],

  function (Node, NodeValue) {
    var api = NodeValue(Node.isText, 'text');

    var get = function (element) {
      return api.get(element);
    };

    var getOption = function (element) {
      return api.getOption(element);
    };

    var set = function (element, value) {
      api.set(element, value);
    };

    return {
      get: get,
      getOption: getOption,
      set: set
    };
  }
);

define(
  'ephox.sugar.api.node.Body',

  [
    'ephox.katamari.api.Thunk',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.node.Node',
    'global!document'
  ],

  function (Thunk, Element, Node, document) {

    // Node.contains() is very, very, very good performance
    // http://jsperf.com/closest-vs-contains/5
    var inBody = function (element) {
      // Technically this is only required on IE, where contains() returns false for text nodes.
      // But it's cheap enough to run everywhere and Sugar doesn't have platform detection (yet).
      var dom = Node.isText(element) ? element.dom().parentNode : element.dom();

      // use ownerDocument.body to ensure this works inside iframes.
      // Normally contains is bad because an element "contains" itself, but here we want that.
      return dom !== undefined && dom !== null && dom.ownerDocument.body.contains(dom);
    };

    var body = Thunk.cached(function() {
      return getBody(Element.fromDom(document));
    });

    var getBody = function (doc) {
      var body = doc.dom().body;
      if (body === null || body === undefined) throw 'Body is not available yet';
      return Element.fromDom(body);
    };

    return {
      body: body,
      getBody: getBody,
      inBody: inBody
    };
  }
);

define(
  'ephox.sugar.api.search.PredicateFilter',

  [
    'ephox.katamari.api.Arr',
    'ephox.sugar.api.node.Body',
    'ephox.sugar.api.search.Traverse'
  ],

  function (Arr, Body, Traverse) {
    // maybe TraverseWith, similar to traverse but with a predicate?

    var all = function (predicate) {
      return descendants(Body.body(), predicate);
    };

    var ancestors = function (scope, predicate, isRoot) {
      return Arr.filter(Traverse.parents(scope, isRoot), predicate);
    };

    var siblings = function (scope, predicate) {
      return Arr.filter(Traverse.siblings(scope), predicate);
    };

    var children = function (scope, predicate) {
      return Arr.filter(Traverse.children(scope), predicate);
    };

    var descendants = function (scope, predicate) {
      var result = [];

      // Recurse.toArray() might help here
      Arr.each(Traverse.children(scope), function (x) {
        if (predicate(x)) {
          result = result.concat([ x ]);
        }
        result = result.concat(descendants(x, predicate));
      });
      return result;
    };

    return {
      all: all,
      ancestors: ancestors,
      siblings: siblings,
      children: children,
      descendants: descendants
    };
  }
);

define(
  'ephox.sugar.api.search.SelectorFilter',

  [
    'ephox.sugar.api.search.PredicateFilter',
    'ephox.sugar.api.search.Selectors'
  ],

  function (PredicateFilter, Selectors) {
    var all = function (selector) {
      return Selectors.all(selector);
    };

    // For all of the following:
    //
    // jQuery does siblings of firstChild. IE9+ supports scope.dom().children (similar to Traverse.children but elements only).
    // Traverse should also do this (but probably not by default).
    //

    var ancestors = function (scope, selector, isRoot) {
      // It may surprise you to learn this is exactly what JQuery does
      // TODO: Avoid all this wrapping and unwrapping
      return PredicateFilter.ancestors(scope, function (e) {
        return Selectors.is(e, selector);
      }, isRoot);
    };

    var siblings = function (scope, selector) {
      // It may surprise you to learn this is exactly what JQuery does
      // TODO: Avoid all the wrapping and unwrapping
      return PredicateFilter.siblings(scope, function (e) {
        return Selectors.is(e, selector);
      });
    };

    var children = function (scope, selector) {
      // It may surprise you to learn this is exactly what JQuery does
      // TODO: Avoid all the wrapping and unwrapping
      return PredicateFilter.children(scope, function (e) {
        return Selectors.is(e, selector);
      });
    };

    var descendants = function (scope, selector) {
      return Selectors.all(selector, scope);
    };

    return {
      all: all,
      ancestors: ancestors,
      siblings: siblings,
      children: children,
      descendants: descendants
    };
  }
);

/**
 * ElementType.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
 */

define(
  'tinymce.core.dom.ElementType',
  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Fun',
    'ephox.sugar.api.node.Node'
  ],
  function (Arr, Fun, Node) {
    var blocks = [
      'article', 'aside', 'details', 'div', 'dt', 'figcaption', 'footer',
      'form', 'fieldset', 'header', 'hgroup', 'html', 'main', 'nav',
      'section', 'summary', 'body', 'p', 'dl', 'multicol', 'dd', 'figure',
      'address', 'center', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
      'listing', 'xmp', 'pre', 'plaintext', 'menu', 'dir', 'ul', 'ol', 'li', 'hr',
      'table', 'tbody', 'thead', 'tfoot', 'th', 'tr', 'td', 'caption'
    ];

    var voids = [
      'area', 'base', 'basefont', 'br', 'col', 'frame', 'hr', 'img', 'input',
      'isindex', 'link', 'meta', 'param', 'embed', 'source', 'wbr', 'track'
    ];

    var tableCells = ['td', 'th'];
    var tableSections = ['thead', 'tbody', 'tfoot'];

    var textBlocks = [
      'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div', 'address', 'pre', 'form',
      'blockquote', 'center', 'dir', 'fieldset', 'header', 'footer', 'article',
      'section', 'hgroup', 'aside', 'nav', 'figure'
    ];

    var headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
    var listItems = ['li', 'dd', 'dt'];
    var lists = ['ul', 'ol', 'dl'];

    var lazyLookup = function (items) {
      var lookup;
      return function (node) {
        lookup = lookup ? lookup : Arr.mapToObject(items, Fun.constant(true));
        return lookup.hasOwnProperty(Node.name(node));
      };
    };

    var isHeading = lazyLookup(headings);

    var isBlock = lazyLookup(blocks);

    var isInline = function (node) {
      return Node.isElement(node) && !isBlock(node);
    };

    var isBr = function (node) {
      return Node.isElement(node) && Node.name(node) === 'br';
    };

    return {
      isBlock: isBlock,
      isInline: isInline,
      isHeading: isHeading,
      isTextBlock: lazyLookup(textBlocks),
      isList: lazyLookup(lists),
      isListItem: lazyLookup(listItems),
      isVoid: lazyLookup(voids),
      isTableSection: lazyLookup(tableSections),
      isTableCell: lazyLookup(tableCells),
      isBr: isBr
    };
  }
);

/**
 * PaddingBr.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
 */

define(
  'tinymce.core.dom.PaddingBr',
  [
    'ephox.katamari.api.Arr',
    'ephox.sugar.api.dom.Insert',
    'ephox.sugar.api.dom.Remove',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.node.Node',
    'ephox.sugar.api.node.Text',
    'ephox.sugar.api.search.SelectorFilter',
    'ephox.sugar.api.search.Traverse',
    'tinymce.core.dom.ElementType'
  ],
  function (Arr, Insert, Remove, Element, Node, Text, SelectorFilter, Traverse, ElementType) {
    var getLastChildren = function (elm) {
      var children = [], rawNode = elm.dom();

      while (rawNode) {
        children.push(Element.fromDom(rawNode));
        rawNode = rawNode.lastChild;
      }

      return children;
    };

    var removeTrailingBr = function (elm) {
      var allBrs = SelectorFilter.descendants(elm, 'br');
      var brs = Arr.filter(getLastChildren(elm).slice(-1), ElementType.isBr);
      if (allBrs.length === brs.length) {
        Arr.each(brs, Remove.remove);
      }
    };

    var fillWithPaddingBr = function (elm) {
      Remove.empty(elm);
      Insert.append(elm, Element.fromHtml('<br data-mce-bogus="1">'));
    };

    var isPaddingContents = function (elm) {
      return Node.isText(elm) ? Text.get(elm) === '\u00a0' : ElementType.isBr(elm);
    };

    var isPaddedElement = function (elm) {
      return Arr.filter(Traverse.children(elm), isPaddingContents).length === 1;
    };

    var trimBlockTrailingBr = function (elm) {
      Traverse.lastChild(elm).each(function (lastChild) {
        Traverse.prevSibling(lastChild).each(function (lastChildPrevSibling) {
          if (ElementType.isBlock(elm) && ElementType.isBr(lastChild) && ElementType.isBlock(lastChildPrevSibling)) {
            Remove.remove(lastChild);
          }
        });
      });
    };

    return {
      removeTrailingBr: removeTrailingBr,
      fillWithPaddingBr: fillWithPaddingBr,
      isPaddedElement: isPaddedElement,
      trimBlockTrailingBr: trimBlockTrailingBr
    };
  }
);
/**
 * FormatUtils.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
 */

define(
  'tinymce.core.fmt.FormatUtils',
  [
    'tinymce.core.dom.TreeWalker'
  ],
  function (TreeWalker) {
    var isInlineBlock = function (node) {
      return node && /^(IMG)$/.test(node.nodeName);
    };

    var moveStart = function (dom, selection, rng) {
      var container = rng.startContainer,
        offset = rng.startOffset,
        walker, node, nodes;

      if (rng.startContainer === rng.endContainer) {
        if (isInlineBlock(rng.startContainer.childNodes[rng.startOffset])) {
          return;
        }
      }

      // Convert text node into index if possible
      if (container.nodeType === 3 && offset >= container.nodeValue.length) {
        // Get the parent container location and walk from there
        offset = dom.nodeIndex(container);
        container = container.parentNode;
      }

      // Move startContainer/startOffset in to a suitable node
      if (container.nodeType === 1) {
        nodes = container.childNodes;
        if (offset < nodes.length) {
          container = nodes[offset];
          walker = new TreeWalker(container, dom.getParent(container, dom.isBlock));
        } else {
          container = nodes[nodes.length - 1];
          walker = new TreeWalker(container, dom.getParent(container, dom.isBlock));
          walker.next(true);
        }

        for (node = walker.current(); node; node = walker.next()) {
          if (node.nodeType === 3 && !isWhiteSpaceNode(node)) {
            rng.setStart(node, 0);
            selection.setRng(rng);

            return;
          }
        }
      }
    };

    /**
     * Returns the next/previous non whitespace node.
     *
     * @private
     * @param {Node} node Node to start at.
     * @param {boolean} next (Optional) Include next or previous node defaults to previous.
     * @param {boolean} inc (Optional) Include the current node in checking. Defaults to false.
     * @return {Node} Next or previous node or undefined if it wasn't found.
     */
    var getNonWhiteSpaceSibling = function (node, next, inc) {
      if (node) {
        next = next ? 'nextSibling' : 'previousSibling';

        for (node = inc ? node : node[next]; node; node = node[next]) {
          if (node.nodeType === 1 || !isWhiteSpaceNode(node)) {
            return node;
          }
        }
      }
    };

    var isTextBlock = function (editor, name) {
      if (name.nodeType) {
        name = name.nodeName;
      }

      return !!editor.schema.getTextBlockElements()[name.toLowerCase()];
    };

    var isValid = function (ed, parent, child) {
      return ed.schema.isValidChild(parent, child);
    };

    var isWhiteSpaceNode = function (node) {
      return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue);
    };

    /**
     * Replaces variables in the value. The variable format is %var.
     *
     * @private
     * @param {String} value Value to replace variables in.
     * @param {Object} vars Name/value array with variables to replace.
     * @return {String} New value with replaced variables.
     */
    var replaceVars = function (value, vars) {
      if (typeof value !== "string") {
        value = value(vars);
      } else if (vars) {
        value = value.replace(/%(\w+)/g, function (str, name) {
          return vars[name] || str;
        });
      }

      return value;
    };

    /**
     * Compares two string/nodes regardless of their case.
     *
     * @private
     * @param {String/Node} str1 Node or string to compare.
     * @param {String/Node} str2 Node or string to compare.
     * @return {boolean} True/false if they match.
     */
    var isEq = function (str1, str2) {
      str1 = str1 || '';
      str2 = str2 || '';

      str1 = '' + (str1.nodeName || str1);
      str2 = '' + (str2.nodeName || str2);

      return str1.toLowerCase() === str2.toLowerCase();
    };

    var normalizeStyleValue = function (dom, value, name) {
      // Force the format to hex
      if (name === 'color' || name === 'backgroundColor') {
        value = dom.toHex(value);
      }

      // Opera will return bold as 700
      if (name === 'fontWeight' && value === 700) {
        value = 'bold';
      }

      // Normalize fontFamily so "'Font name', Font" becomes: "Font name,Font"
      if (name === 'fontFamily') {
        value = value.replace(/[\'\"]/g, '').replace(/,\s+/g, ',');
      }

      return '' + value;
    };

    var getStyle = function (dom, node, name) {
      return normalizeStyleValue(dom, dom.getStyle(node, name), name);
    };

    var getTextDecoration = function (dom, node) {
      var decoration;

      dom.getParent(node, function (n) {
        decoration = dom.getStyle(n, 'text-decoration');
        return decoration && decoration !== 'none';
      });

      return decoration;
    };

    var getParents = function (dom, node, selector) {
      return dom.getParents(node, selector, dom.getRoot());
    };

    return {
      isInlineBlock: isInlineBlock,
      moveStart: moveStart,
      getNonWhiteSpaceSibling: getNonWhiteSpaceSibling,
      isTextBlock: isTextBlock,
      isValid: isValid,
      isWhiteSpaceNode: isWhiteSpaceNode,
      replaceVars: replaceVars,
      isEq: isEq,
      normalizeStyleValue: normalizeStyleValue,
      getStyle: getStyle,
      getTextDecoration: getTextDecoration,
      getParents: getParents
    };
  }
);
/**
 * ExpandRange.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
 */

define(
  'tinymce.core.fmt.ExpandRange',
  [
    'tinymce.core.dom.BookmarkManager',
    'tinymce.core.dom.TreeWalker',
    'tinymce.core.fmt.FormatUtils'
  ],
  function (BookmarkManager, TreeWalker, FormatUtils) {
    var isBookmarkNode = BookmarkManager.isBookmarkNode;
    var getParents = FormatUtils.getParents, isWhiteSpaceNode = FormatUtils.isWhiteSpaceNode, isTextBlock = FormatUtils.isTextBlock;

    // This function walks down the tree to find the leaf at the selection.
      // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node.
    var findLeaf = function (node, offset) {
      if (typeof offset === 'undefined') {
        offset = node.nodeType === 3 ? node.length : node.childNodes.length;
      }

      while (node && node.hasChildNodes()) {
        node = node.childNodes[offset];
        if (node) {
          offset = node.nodeType === 3 ? node.length : node.childNodes.length;
        }
      }
      return { node: node, offset: offset };
    };

    var excludeTrailingWhitespace = function (endContainer, endOffset) {
      // Avoid applying formatting to a trailing space,
      // but remove formatting from trailing space
      var leaf = findLeaf(endContainer, endOffset);
      if (leaf.node) {
        while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) {
          leaf = findLeaf(leaf.node.previousSibling);
        }

        if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 &&
            leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') {

          if (leaf.offset > 1) {
            endContainer = leaf.node;
            endContainer.splitText(leaf.offset - 1);
          }
        }
      }

      return endContainer;
    };

    var isBogusBr = function (node) {
      return node.nodeName === "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling;
    };

    var expandRng = function (editor, rng, format, remove) {
      var lastIdx, endPoint,
        startContainer = rng.startContainer,
        startOffset = rng.startOffset,
        endContainer = rng.endContainer,
        endOffset = rng.endOffset,
        dom = editor.dom;

      // This function walks up the tree if there is no siblings before/after the node
      var findParentContainer = function (start) {
        var container, parent, sibling, siblingName, root;

        container = parent = start ? startContainer : endContainer;
        siblingName = start ? 'previousSibling' : 'nextSibling';
        root = dom.getRoot();

        // If it's a text node and the offset is inside the text
        if (container.nodeType === 3 && !isWhiteSpaceNode(container)) {
          if (start ? startOffset > 0 : endOffset < container.nodeValue.length) {
            return container;
          }
        }

        /*eslint no-constant-condition:0 */
        while (true) {
          // Stop expanding on block elements
          if (!format[0].block_expand && dom.isBlock(parent)) {
            return parent;
          }

          // Walk left/right
          for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) {
            if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) {
              return parent;
            }
          }

          // Check if we can move up are we at root level or body level
          if (parent === root || parent.parentNode === root) {
            container = parent;
            break;
          }

          parent = parent.parentNode;
        }

        return container;
      };

      // If index based start position then resolve it
      if (startContainer.nodeType === 1 && startContainer.hasChildNodes()) {
        lastIdx = startContainer.childNodes.length - 1;
        startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset];

        if (startContainer.nodeType === 3) {
          startOffset = 0;
        }
      }

      // If index based end position then resolve it
      if (endContainer.nodeType === 1 && endContainer.hasChildNodes()) {
        lastIdx = endContainer.childNodes.length - 1;
        endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1];

        if (endContainer.nodeType === 3) {
          endOffset = endContainer.nodeValue.length;
        }
      }

      // Expands the node to the closes contentEditable false element if it exists
      var findParentContentEditable = function (node) {
        var parent = node;

        while (parent) {
          if (parent.nodeType === 1 && dom.getContentEditable(parent)) {
            return dom.getContentEditable(parent) === "false" ? parent : node;
          }

          parent = parent.parentNode;
        }

        return node;
      };

      var findWordEndPoint = function (container, offset, start) {
        var walker, node, pos, lastTextNode;

        var findSpace = function (node, offset) {
          var pos, pos2, str = node.nodeValue;

          if (typeof offset === "undefined") {
            offset = start ? str.length : 0;
          }

          if (start) {
            pos = str.lastIndexOf(' ', offset);
            pos2 = str.lastIndexOf('\u00a0', offset);
            pos = pos > pos2 ? pos : pos2;

            // Include the space on remove to avoid tag soup
            if (pos !== -1 && !remove) {
              pos++;
            }
          } else {
            pos = str.indexOf(' ', offset);
            pos2 = str.indexOf('\u00a0', offset);
            pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2;
          }

          return pos;
        };

        if (container.nodeType === 3) {
          pos = findSpace(container, offset);

          if (pos !== -1) {
            return { container: container, offset: pos };
          }

          lastTextNode = container;
        }

        // Walk the nodes inside the block
        walker = new TreeWalker(container, dom.getParent(container, dom.isBlock) || editor.getBody());
        while ((node = walker[start ? 'prev' : 'next']())) {
          if (node.nodeType === 3) {
            lastTextNode = node;
            pos = findSpace(node);

            if (pos !== -1) {
              return { container: node, offset: pos };
            }
          } else if (dom.isBlock(node)) {
            break;
          }
        }

        if (lastTextNode) {
          if (start) {
            offset = 0;
          } else {
            offset = lastTextNode.length;
          }

          return { container: lastTextNode, offset: offset };
        }
      };

      var findSelectorEndPoint = function (container, siblingName) {
        var parents, i, y, curFormat;

        if (container.nodeType === 3 && container.nodeValue.length === 0 && container[siblingName]) {
          container = container[siblingName];
        }

        parents = getParents(dom, container);
        for (i = 0; i < parents.length; i++) {
          for (y = 0; y < format.length; y++) {
            curFormat = format[y];

            // If collapsed state is set then skip formats that doesn't match that
            if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) {
              continue;
            }

            if (dom.is(parents[i], curFormat.selector)) {
              return parents[i];
            }
          }
        }

        return container;
      };

      var findBlockEndPoint = function (container, siblingName) {
        var node, root = dom.getRoot();

        // Expand to block of similar type
        if (!format[0].wrapper) {
          node = dom.getParent(container, format[0].block, root);
        }

        // Expand to first wrappable block element or any block element
        if (!node) {
          var scopeRoot = dom.getParent(container, 'LI,TD,TH');
          node = dom.getParent(container.nodeType === 3 ? container.parentNode : container, function (node) {
            // Fixes #6183 where it would expand to editable parent element in inline mode
            return node !== root && isTextBlock(editor, node);
          }, scopeRoot);
        }

        // Exclude inner lists from wrapping
        if (node && format[0].wrapper) {
          node = getParents(dom, node, 'ul,ol').reverse()[0] || node;
        }

        // Didn't find a block element look for first/last wrappable element
        if (!node) {
          node = container;

          while (node[siblingName] && !dom.isBlock(node[siblingName])) {
            node = node[siblingName];

            // Break on BR but include it will be removed later on
            // we can't remove it now since we need to check if it can be wrapped
            if (FormatUtils.isEq(node, 'br')) {
              break;
            }
          }
        }

        return node || container;
      };

      // Expand to closest contentEditable element
      startContainer = findParentContentEditable(startContainer);
      endContainer = findParentContentEditable(endContainer);

      // Exclude bookmark nodes if possible
      if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) {
        startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode;
        startContainer = startContainer.nextSibling || startContainer;

        if (startContainer.nodeType === 3) {
          startOffset = 0;
        }
      }

      if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) {
        endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode;
        endContainer = endContainer.previousSibling || endContainer;

        if (endContainer.nodeType === 3) {
          endOffset = endContainer.length;
        }
      }

      if (format[0].inline) {
        if (rng.collapsed) {
          // Expand left to closest word boundary
          endPoint = findWordEndPoint(startContainer, startOffset, true);
          if (endPoint) {
            startContainer = endPoint.container;
            startOffset = endPoint.offset;
          }

          // Expand right to closest word boundary
          endPoint = findWordEndPoint(endContainer, endOffset);
          if (endPoint) {
            endContainer = endPoint.container;
            endOffset = endPoint.offset;
          }
        }

        endContainer = remove ? endContainer : excludeTrailingWhitespace(endContainer, endOffset);
      }

      // Move start/end point up the tree if the leaves are sharp and if we are in different containers
      // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>!
      // This will reduce the number of wrapper elements that needs to be created
      // Move start point up the tree
      if (format[0].inline || format[0].block_expand) {
        if (!format[0].inline || (startContainer.nodeType !== 3 || startOffset === 0)) {
          startContainer = findParentContainer(true);
        }

        if (!format[0].inline || (endContainer.nodeType !== 3 || endOffset === endContainer.nodeValue.length)) {
          endContainer = findParentContainer();
        }
      }

      // Expand start/end container to matching selector
      if (format[0].selector && format[0].expand !== false && !format[0].inline) {
        // Find new startContainer/endContainer if there is better one
        startContainer = findSelectorEndPoint(startContainer, 'previousSibling');
        endContainer = findSelectorEndPoint(endContainer, 'nextSibling');
      }

      // Expand start/end container to matching block element or text node
      if (format[0].block || format[0].selector) {
        // Find new startContainer/endContainer if there is better one
        startContainer = findBlockEndPoint(startContainer, 'previousSibling');
        endContainer = findBlockEndPoint(endContainer, 'nextSibling');

        // Non block element then try to expand up the leaf
        if (format[0].block) {
          if (!dom.isBlock(startContainer)) {
            startContainer = findParentContainer(true);
          }

          if (!dom.isBlock(endContainer)) {
            endContainer = findParentContainer();
          }
        }
      }

      // Setup index for startContainer
      if (startContainer.nodeType === 1) {
        startOffset = dom.nodeIndex(startContainer);
        startContainer = startContainer.parentNode;
      }

      // Setup index for endContainer
      if (endContainer.nodeType === 1) {
        endOffset = dom.nodeIndex(endContainer) + 1;
        endContainer = endContainer.parentNode;
      }

      // Return new range like object
      return {
        startContainer: startContainer,
        startOffset: startOffset,
        endContainer: endContainer,
        endOffset: endOffset
      };
    };

    return {
      expandRng: expandRng
    };
  }
);
/**
 * MatchFormat.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
 */

define(
  'tinymce.core.fmt.MatchFormat',
  [
    'tinymce.core.fmt.FormatUtils'
  ],
  function (FormatUtils) {
    var isEq = FormatUtils.isEq;

    var matchesUnInheritedFormatSelector = function (ed, node, name) {
      var formatList = ed.formatter.get(name);

      if (formatList) {
        for (var i = 0; i < formatList.length; i++) {
          if (formatList[i].inherit === false && ed.dom.is(node, formatList[i].selector)) {
            return true;
          }
        }
      }

      return false;
    };

    var matchParents = function (editor, node, name, vars) {
      var root = editor.dom.getRoot();

      if (node === root) {
        return false;
      }

      // Find first node with similar format settings
      node = editor.dom.getParent(node, function (node) {
        if (matchesUnInheritedFormatSelector(editor, node, name)) {
          return true;
        }

        return node.parentNode === root || !!matchNode(editor, node, name, vars, true);
      });

      // Do an exact check on the similar format element
      return matchNode(editor, node, name, vars);
    };

    var matchName = function (dom, node, format) {
      // Check for inline match
      if (isEq(node, format.inline)) {
        return true;
      }

      // Check for block match
      if (isEq(node, format.block)) {
        return true;
      }

      // Check for selector match
      if (format.selector) {
        return node.nodeType === 1 && dom.is(node, format.selector);
      }
    };

    var matchItems = function (dom, node, format, itemName, similar, vars) {
      var key, value, items = format[itemName], i;

      // Custom match
      if (format.onmatch) {
        return format.onmatch(node, format, itemName);
      }

      // Check all items
      if (items) {
        // Non indexed object
        if (typeof items.length === 'undefined') {
          for (key in items) {
            if (items.hasOwnProperty(key)) {
              if (itemName === 'attributes') {
                value = dom.getAttrib(node, key);
              } else {
                value = FormatUtils.getStyle(dom, node, key);
              }

              if (similar && !value && !format.exact) {
                return;
              }

              if ((!similar || format.exact) && !isEq(value, FormatUtils.normalizeStyleValue(dom, FormatUtils.replaceVars(items[key], vars), key))) {
                return;
              }
            }
          }
        } else {
          // Only one match needed for indexed arrays
          for (i = 0; i < items.length; i++) {
            if (itemName === 'attributes' ? dom.getAttrib(node, items[i]) : FormatUtils.getStyle(dom, node, items[i])) {
              return format;
            }
          }
        }
      }

      return format;
    };

    var matchNode = function (ed, node, name, vars, similar) {
      var formatList = ed.formatter.get(name), format, i, x, classes, dom = ed.dom;

      if (formatList && node) {
        // Check each format in list
        for (i = 0; i < formatList.length; i++) {
          format = formatList[i];

          // Name name, attributes, styles and classes
          if (matchName(ed.dom, node, format) && matchItems(dom, node, format, 'attributes', similar, vars) && matchItems(dom, node, format, 'styles', similar, vars)) {
            // Match classes
            if ((classes = format.classes)) {
              for (x = 0; x < classes.length; x++) {
                if (!ed.dom.hasClass(node, classes[x])) {
                  return;
                }
              }
            }

            return format;
          }
        }
      }
    };

    var match = function (editor, name, vars, node) {
      var startNode;

      // Check specified node
      if (node) {
        return matchParents(editor, node, name, vars);
      }

      // Check selected node
      node = editor.selection.getNode();
      if (matchParents(editor, node, name, vars)) {
        return true;
      }

      // Check start node if it's different
      startNode = editor.selection.getStart();
      if (startNode !== node) {
        if (matchParents(editor, startNode, name, vars)) {
          return true;
        }
      }

      return false;
    };

    var matchAll = function (editor, names, vars) {
      var startElement, matchedFormatNames = [], checkedMap = {};

      // Check start of selection for formats
      startElement = editor.selection.getStart();
      editor.dom.getParent(startElement, function (node) {
        var i, name;

        for (i = 0; i < names.length; i++) {
          name = names[i];

          if (!checkedMap[name] && matchNode(editor, node, name, vars)) {
            checkedMap[name] = true;
            matchedFormatNames.push(name);
          }
        }
      }, editor.dom.getRoot());

      return matchedFormatNames;
    };

    var canApply = function (editor, name) {
      var formatList = editor.formatter.get(name), startNode, parents, i, x, selector, dom = editor.dom;

      if (formatList) {
        startNode = editor.selection.getStart();
        parents = FormatUtils.getParents(dom, startNode);

        for (x = formatList.length - 1; x >= 0; x--) {
          selector = formatList[x].selector;

          // Format is not selector based then always return TRUE
          // Is it has a defaultBlock then it's likely it can be applied for example align on a non block element line
          if (!selector || formatList[x].defaultBlock) {
            return true;
          }

          for (i = parents.length - 1; i >= 0; i--) {
            if (dom.is(parents[i], selector)) {
              return true;
            }
          }
        }
      }

      return false;
    };

    return {
      matchNode: matchNode,
      matchName: matchName,
      match: match,
      matchAll: matchAll,
      canApply: canApply,
      matchesUnInheritedFormatSelector: matchesUnInheritedFormatSelector
    };
  }
);
/**
 * CaretFormat.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
 */

define(
  'tinymce.core.fmt.CaretFormat',
  [
    'ephox.katamari.api.Arr',
    'ephox.sugar.api.node.Element',
    'tinymce.core.dom.PaddingBr',
    'tinymce.core.dom.RangeUtils',
    'tinymce.core.dom.TreeWalker',
    'tinymce.core.fmt.ExpandRange',
    'tinymce.core.fmt.FormatUtils',
    'tinymce.core.fmt.MatchFormat',
    'tinymce.core.text.Zwsp',
    'tinymce.core.util.Fun',
    'tinymce.core.util.Tools'
  ],
  function (Arr, Element, PaddingBr, RangeUtils, TreeWalker, ExpandRange, FormatUtils, MatchFormat, Zwsp, Fun, Tools) {
    var ZWSP = Zwsp.ZWSP, CARET_ID = '_mce_caret', DEBUG = false;

    var isCaretNode = function (node) {
      return node.nodeType === 1 && node.id === CARET_ID;
    };

    var isCaretContainerEmpty = function (node, nodes) {
      while (node) {
        if ((node.nodeType === 3 && node.nodeValue !== ZWSP) || node.childNodes.length > 1) {
          return false;
        }

        // Collect nodes
        if (nodes && node.nodeType === 1) {
          nodes.push(node);
        }

        node = node.firstChild;
      }

      return true;
    };

    var findFirstTextNode = function (node) {
      var walker;

      if (node) {
        walker = new TreeWalker(node, node);

        for (node = walker.current(); node; node = walker.next()) {
          if (node.nodeType === 3) {
            return node;
          }
        }
      }

      return null;
    };

    var createCaretContainer = function (dom, fill) {
      var caretContainer = dom.create('span', { id: CARET_ID, 'data-mce-bogus': '1', style: DEBUG ? 'color:red' : '' });

      if (fill) {
        caretContainer.appendChild(dom.doc.createTextNode(ZWSP));
      }

      return caretContainer;
    };

    var getParentCaretContainer = function (node) {
      while (node) {
        if (node.id === CARET_ID) {
          return node;
        }

        node = node.parentNode;
      }
    };

    // Checks if the parent caret container node isn't empty if that is the case it
    // will remove the bogus state on all children that isn't empty
    var unmarkBogusCaretParents = function (dom, selection) {
      var caretContainer;

      caretContainer = getParentCaretContainer(selection.getStart());
      if (caretContainer && !dom.isEmpty(caretContainer)) {
        Tools.walk(caretContainer, function (node) {
          if (node.nodeType === 1 && node.id !== CARET_ID && !dom.isEmpty(node)) {
            dom.setAttrib(node, 'data-mce-bogus', null);
          }
        }, 'childNodes');
      }
    };

    var trimZwspFromCaretContainer = function (caretContainerNode) {
      var textNode = findFirstTextNode(caretContainerNode);
      if (textNode && textNode.nodeValue.charAt(0) === ZWSP) {
        textNode.deleteData(0, 1);
      }

      return textNode;
    };

    var removeCaretContainerNode = function (dom, selection, node, moveCaret) {
      var rng, block, textNode;

      rng = selection.getRng(true);
      block = dom.getParent(node, dom.isBlock);

      if (isCaretContainerEmpty(node)) {
        if (moveCaret !== false) {
          rng.setStartBefore(node);
          rng.setEndBefore(node);
        }

        dom.remove(node);
      } else {
        textNode = trimZwspFromCaretContainer(node);
        if (rng.startContainer === textNode && rng.startOffset > 0) {
          rng.setStart(textNode, rng.startOffset - 1);
        }

        if (rng.endContainer === textNode && rng.endOffset > 0) {
          rng.setEnd(textNode, rng.endOffset - 1);
        }

        dom.remove(node, true);
      }

      if (block && dom.isEmpty(block)) {
        PaddingBr.fillWithPaddingBr(Element.fromDom(block));
      }

      selection.setRng(rng);
    };

    // Removes the caret container for the specified node or all on the current document
    var removeCaretContainer = function (dom, selection, node, moveCaret) {
      if (!node) {
        node = getParentCaretContainer(selection.getStart());

        if (!node) {
          while ((node = dom.get(CARET_ID))) {
            removeCaretContainerNode(dom, selection, node, false);
          }
        }
      } else {
        removeCaretContainerNode(dom, selection, node, moveCaret);
      }
    };

    var insertCaretContainerNode = function (editor, caretContainer, formatNode) {
      var dom = editor.dom, block = dom.getParent(formatNode, Fun.curry(FormatUtils.isTextBlock, editor));

      if (block && dom.isEmpty(block)) {
        // Replace formatNode with caretContainer when removing format from empty block like <p><b>|</b></p>
        formatNode.parentNode.replaceChild(caretContainer, formatNode);
      } else {
        PaddingBr.removeTrailingBr(Element.fromDom(formatNode));
        if (dom.isEmpty(formatNode)) {
          formatNode.parentNode.replaceChild(caretContainer, formatNode);
        } else {
          dom.insertAfter(caretContainer, formatNode);
        }
      }
    };

    var appendNode = function (parentNode, node) {
      parentNode.appendChild(node);
      return node;
    };

    var insertFormatNodesIntoCaretContainer = function (formatNodes, caretContainer) {
      var innerMostFormatNode = Arr.foldr(formatNodes, function (parentNode, formatNode) {
        return appendNode(parentNode, formatNode.cloneNode(false));
      }, caretContainer);

      return appendNode(innerMostFormatNode, innerMostFormatNode.ownerDocument.createTextNode(ZWSP));
    };

    var setupCaretEvents = function (editor) {
      if (!editor._hasCaretEvents) {
        bindEvents(editor);
        editor._hasCaretEvents = true;
      }
    };

    var applyCaretFormat = function (editor, name, vars) {
      var rng, caretContainer, textNode, offset, bookmark, container, text;
      var dom = editor.dom, selection = editor.selection;

      setupCaretEvents(editor);

      rng = selection.getRng(true);
      offset = rng.startOffset;
      container = rng.startContainer;
      text = container.nodeValue;

      caretContainer = getParentCaretContainer(selection.getStart());
      if (caretContainer) {
        textNode = findFirstTextNode(caretContainer);
      }

      // Expand to word if caret is in the middle of a text node and the char before/after is a alpha numeric character
      var wordcharRegex = /[^\s\u00a0\u00ad\u200b\ufeff]/;
      if (text && offset > 0 && offset < text.length &&
        wordcharRegex.test(text.charAt(offset)) && wordcharRegex.test(text.charAt(offset - 1))) {
        // Get bookmark of caret position
        bookmark = selection.getBookmark();

        // Collapse bookmark range (WebKit)
        rng.collapse(true);

        // Expand the range to the closest word and split it at those points
        rng = ExpandRange.expandRng(editor, rng, editor.formatter.get(name));
        rng = new RangeUtils(dom).split(rng);

        // Apply the format to the range
        editor.formatter.apply(name, vars, rng);

        // Move selection back to caret position
        selection.moveToBookmark(bookmark);
      } else {
        if (!caretContainer || textNode.nodeValue !== ZWSP) {
          caretContainer = createCaretContainer(dom, true);
          textNode = caretContainer.firstChild;

          rng.insertNode(caretContainer);
          offset = 1;

          editor.formatter.apply(name, vars, caretContainer);
        } else {
          editor.formatter.apply(name, vars, caretContainer);
        }

        // Move selection to text node
        selection.setCursorLocation(textNode, offset);
      }
    };

    var removeCaretFormat = function (editor, name, vars, similar) {
      var dom = editor.dom, selection = editor.selection;
      var rng = selection.getRng(true), container, offset, bookmark;
      var hasContentAfter, node, formatNode, parents = [], caretContainer;

      setupCaretEvents(editor);

      container = rng.startContainer;
      offset = rng.startOffset;
      node = container;

      if (container.nodeType === 3) {
        if (offset !== container.nodeValue.length) {
          hasContentAfter = true;
        }

        node = node.parentNode;
      }

      while (node) {
        if (MatchFormat.matchNode(editor, node, name, vars, similar)) {
          formatNode = node;
          break;
        }

        if (node.nextSibling) {
          hasContentAfter = true;
        }

        parents.push(node);
        node = node.parentNode;
      }

      // Node doesn't have the specified format
      if (!formatNode) {
        return;
      }

      // Is there contents after the caret then remove the format on the element
      if (hasContentAfter) {
        bookmark = selection.getBookmark();

        // Collapse bookmark range (WebKit)
        rng.collapse(true);

        // Expand the range to the closest word and split it at those points
        rng = ExpandRange.expandRng(editor, rng, editor.formatter.get(name), true);
        rng = new RangeUtils(dom).split(rng);

        editor.formatter.remove(name, vars, rng);
        selection.moveToBookmark(bookmark);
      } else {
        caretContainer = getParentCaretContainer(formatNode);
        var newCaretContainer = createCaretContainer(dom, false);
        var caretNode = insertFormatNodesIntoCaretContainer(parents, newCaretContainer);

        if (caretContainer) {
          insertCaretContainerNode(editor, newCaretContainer, caretContainer);
        } else {
          insertCaretContainerNode(editor, newCaretContainer, formatNode);
        }

        removeCaretContainerNode(dom, selection, caretContainer, false);
        selection.setCursorLocation(caretNode, 1);

        if (dom.isEmpty(formatNode)) {
          dom.remove(formatNode);
        }
      }
    };

    var bindEvents = function (editor) {
      var dom = editor.dom, selection = editor.selection;

      if (!editor._hasCaretEvents) {
        var markCaretContainersBogus, disableCaretContainer;

        editor.on('BeforeGetContent', function (e) {
          if (markCaretContainersBogus && e.format !== 'raw') {
            markCaretContainersBogus();
          }
        });

        editor.on('mouseup keydown', function (e) {
          if (disableCaretContainer) {
            disableCaretContainer(e);
          }
        });

        // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements
        markCaretContainersBogus = function () {
          var nodes = [], i;

          if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) {
            // Mark children
            i = nodes.length;
            while (i--) {
              dom.setAttrib(nodes[i], 'data-mce-bogus', '1');
            }
          }
        };

        disableCaretContainer = function (e) {
          var keyCode = e.keyCode;

          removeCaretContainer(dom, selection, null, false);

          // Remove caret container if it's empty
          if (keyCode === 8 && selection.isCollapsed() && selection.getStart().innerHTML === ZWSP) {
            removeCaretContainer(dom, selection, getParentCaretContainer(selection.getStart()));
          }

          // Remove caret container on keydown and it's left/right arrow keys
          if (keyCode === 37 || keyCode === 39) {
            removeCaretContainer(dom, selection, getParentCaretContainer(selection.getStart()));
          }

          unmarkBogusCaretParents(dom, selection);
        };

        // Remove bogus state if they got filled by contents using editor.selection.setContent
        editor.on('SetContent', function (e) {
          if (e.selection) {
            unmarkBogusCaretParents(dom, selection);
          }
        });
        editor._hasCaretEvents = true;
      }
    };

    return {
      applyCaretFormat: applyCaretFormat,
      removeCaretFormat: removeCaretFormat,
      isCaretNode: isCaretNode
    };
  }
);
/**
 * Hooks.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
 */

/**
 * Internal class for overriding formatting.
 *
 * @private
 * @class tinymce.fmt.Hooks
 */
define(
  'tinymce.core.fmt.Hooks',
  [
    "tinymce.core.util.Arr",
    "tinymce.core.dom.NodeType",
    "tinymce.core.dom.DomQuery"
  ],
  function (Arr, NodeType, $) {
    var postProcessHooks = {}, filter = Arr.filter, each = Arr.each;

    var addPostProcessHook = function (name, hook) {
      var hooks = postProcessHooks[name];

      if (!hooks) {
        postProcessHooks[name] = hooks = [];
      }

      postProcessHooks[name].push(hook);
    };

    var postProcess = function (name, editor) {
      each(postProcessHooks[name], function (hook) {
        hook(editor);
      });
    };

    addPostProcessHook("pre", function (editor) {
      var rng = editor.selection.getRng(), isPre, blocks;

      var hasPreSibling = function (pre) {
        return isPre(pre.previousSibling) && Arr.indexOf(blocks, pre.previousSibling) !== -1;
      };

      var joinPre = function (pre1, pre2) {
        $(pre2).remove();
        $(pre1).append('<br><br>').append(pre2.childNodes);
      };

      isPre = NodeType.matchNodeNames('pre');

      if (!rng.collapsed) {
        blocks = editor.selection.getSelectedBlocks();

        each(filter(filter(blocks, isPre), hasPreSibling), function (pre) {
          joinPre(pre.previousSibling, pre);
        });
      }
    });

    return {
      postProcess: postProcess
    };
  }
);

/**
 * ElementUtils.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
 */

/**
 * Utility class for various element specific functions.
 *
 * @private
 * @class tinymce.dom.ElementUtils
 */
define(
  'tinymce.core.dom.ElementUtils',
  [
    "tinymce.core.dom.BookmarkManager",
    "tinymce.core.util.Tools"
  ],
  function (BookmarkManager, Tools) {
    var each = Tools.each;

    var ElementUtils = function (dom) {
      /**
       * Compares two nodes and checks if it's attributes and styles matches.
       * This doesn't compare classes as items since their order is significant.
       *
       * @method compare
       * @param {Node} node1 First node to compare with.
       * @param {Node} node2 Second node to compare with.
       * @return {boolean} True/false if the nodes are the same or not.
       */
      this.compare = function (node1, node2) {
        // Not the same name
        if (node1.nodeName != node2.nodeName) {
          return false;
        }

        /**
         * Returns all the nodes attributes excluding internal ones, styles and classes.
         *
         * @private
         * @param {Node} node Node to get attributes from.
         * @return {Object} Name/value object with attributes and attribute values.
         */
        var getAttribs = function (node) {
          var attribs = {};

          each(dom.getAttribs(node), function (attr) {
            var name = attr.nodeName.toLowerCase();

            // Don't compare internal attributes or style
            if (name.indexOf('_') !== 0 && name !== 'style' && name.indexOf('data-') !== 0) {
              attribs[name] = dom.getAttrib(node, name);
            }
          });

          return attribs;
        };

        /**
         * Compares two objects checks if it's key + value exists in the other one.
         *
         * @private
         * @param {Object} obj1 First object to compare.
         * @param {Object} obj2 Second object to compare.
         * @return {boolean} True/false if the objects matches or not.
         */
        var compareObjects = function (obj1, obj2) {
          var value, name;

          for (name in obj1) {
            // Obj1 has item obj2 doesn't have
            if (obj1.hasOwnProperty(name)) {
              value = obj2[name];

              // Obj2 doesn't have obj1 item
              if (typeof value == "undefined") {
                return false;
              }

              // Obj2 item has a different value
              if (obj1[name] != value) {
                return false;
              }

              // Delete similar value
              delete obj2[name];
            }
          }

          // Check if obj 2 has something obj 1 doesn't have
          for (name in obj2) {
            // Obj2 has item obj1 doesn't have
            if (obj2.hasOwnProperty(name)) {
              return false;
            }
          }

          return true;
        };

        // Attribs are not the same
        if (!compareObjects(getAttribs(node1), getAttribs(node2))) {
          return false;
        }

        // Styles are not the same
        if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) {
          return false;
        }

        return !BookmarkManager.isBookmarkNode(node1) && !BookmarkManager.isBookmarkNode(node2);
      };
    };

    return ElementUtils;
  }
);

/**
 * RemoveFormat.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
 */

define(
  'tinymce.core.fmt.RemoveFormat',
  [
    'ephox.katamari.api.Fun',
    'tinymce.core.dom.BookmarkManager',
    'tinymce.core.dom.RangeUtils',
    'tinymce.core.dom.TreeWalker',
    'tinymce.core.fmt.CaretFormat',
    'tinymce.core.fmt.ExpandRange',
    'tinymce.core.fmt.FormatUtils',
    'tinymce.core.fmt.MatchFormat',
    'tinymce.core.util.Tools'
  ],
  function (Fun, BookmarkManager, RangeUtils, TreeWalker, CaretFormat, ExpandRange, FormatUtils, MatchFormat, Tools) {
    var MCE_ATTR_RE = /^(src|href|style)$/;
    var each = Tools.each;
    var isEq = FormatUtils.isEq;

    var isTableCell = function (node) {
      return /^(TH|TD)$/.test(node.nodeName);
    };

    var getContainer = function (ed, rng, start) {
      var container, offset, lastIdx;

      container = rng[start ? 'startContainer' : 'endContainer'];
      offset = rng[start ? 'startOffset' : 'endOffset'];

      if (container.nodeType === 1) {
        lastIdx = container.childNodes.length - 1;

        if (!start && offset) {
          offset--;
        }

        container = container.childNodes[offset > lastIdx ? lastIdx : offset];
      }

      // If start text node is excluded then walk to the next node
      if (container.nodeType === 3 && start && offset >= container.nodeValue.length) {
        container = new TreeWalker(container, ed.getBody()).next() || container;
      }

      // If end text node is excluded then walk to the previous node
      if (container.nodeType === 3 && !start && offset === 0) {
        container = new TreeWalker(container, ed.getBody()).prev() || container;
      }

      return container;
    };

    var wrap = function (dom, node, name, attrs) {
      var wrapper = dom.create(name, attrs);

      node.parentNode.insertBefore(wrapper, node);
      wrapper.appendChild(node);

      return wrapper;
    };

    /**
     * Checks if the specified nodes name matches the format inline/block or selector.
     *
     * @private
     * @param {Node} node Node to match against the specified format.
     * @param {Object} format Format object o match with.
     * @return {boolean} true/false if the format matches.
     */
    var matchName = function (dom, node, format) {
      // Check for inline match
      if (isEq(node, format.inline)) {
        return true;
      }

      // Check for block match
      if (isEq(node, format.block)) {
        return true;
      }

      // Check for selector match
      if (format.selector) {
        return node.nodeType === 1 && dom.is(node, format.selector);
      }
    };

    var isColorFormatAndAnchor = function (node, format) {
      return format.links && node.tagName === 'A';
    };

    var find = function (dom, node, next, inc) {
      node = FormatUtils.getNonWhiteSpaceSibling(node, next, inc);
      return !node || (node.nodeName === 'BR' || dom.isBlock(node));
    };

    /**
     * Removes the node and wrap it's children in paragraphs before doing so or
     * appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled.
     *
     * If the div in the node below gets removed:
     *  text<div>text</div>text
     *
     * Output becomes:
     *  text<div><br />text<br /></div>text
     *
     * So when the div is removed the result is:
     *  text<br />text<br />text
     *
     * @private
     * @param {Node} node Node to remove + apply BR/P elements to.
     * @param {Object} format Format rule.
     * @return {Node} Input node.
     */
    var removeNode = function (ed, node, format) {
      var parentNode = node.parentNode, rootBlockElm;
      var dom = ed.dom, forcedRootBlock = ed.settings.forced_root_block;

      if (format.block) {
        if (!forcedRootBlock) {
          // Append BR elements if needed before we remove the block
          if (dom.isBlock(node) && !dom.isBlock(parentNode)) {
            if (!find(dom, node, false) && !find(dom, node.firstChild, true, 1)) {
              node.insertBefore(dom.create('br'), node.firstChild);
            }

            if (!find(dom, node, true) && !find(dom, node.lastChild, false, 1)) {
              node.appendChild(dom.create('br'));
            }
          }
        } else {
          // Wrap the block in a forcedRootBlock if we are at the root of document
          if (parentNode === dom.getRoot()) {
            if (!format.list_block || !isEq(node, format.list_block)) {
              each(Tools.grep(node.childNodes), function (node) {
                if (FormatUtils.isValid(ed, forcedRootBlock, node.nodeName.toLowerCase())) {
                  if (!rootBlockElm) {
                    rootBlockElm = wrap(dom, node, forcedRootBlock);
                    dom.setAttribs(rootBlockElm, ed.settings.forced_root_block_attrs);
                  } else {
                    rootBlockElm.appendChild(node);
                  }
                } else {
                  rootBlockElm = 0;
                }
              });
            }
          }
        }
      }

      // Never remove nodes that isn't the specified inline element if a selector is specified too
      if (format.selector && format.inline && !isEq(format.inline, node)) {
        return;
      }

      dom.remove(node, 1);
    };

    /**
     * Removes the specified format for the specified node. It will also remove the node if it doesn't have
     * any attributes if the format specifies it to do so.
     *
     * @private
     * @param {Object} format Format object with items to remove from node.
     * @param {Object} vars Name/value object with variables to apply to format.
     * @param {Node} node Node to remove the format styles on.
     * @param {Node} compareNode Optional compare node, if specified the styles will be compared to that node.
     * @return {Boolean} True/false if the node was removed or not.
     */
    var removeFormat = function (ed, format, vars, node, compareNode) {
      var i, attrs, stylesModified, dom = ed.dom;

      // Check if node matches format
      if (!matchName(dom, node, format) && !isColorFormatAndAnchor(node, format)) {
        return false;
      }

      // Should we compare with format attribs and styles
      if (format.remove !== 'all') {
        // Remove styles
        each(format.styles, function (value, name) {
          value = FormatUtils.normalizeStyleValue(dom, FormatUtils.replaceVars(value, vars), name);

          // Indexed array
          if (typeof name === 'number') {
            name = value;
            compareNode = 0;
          }

          if (format.remove_similar || (!compareNode || isEq(FormatUtils.getStyle(dom, compareNode, name), value))) {
            dom.setStyle(node, name, '');
          }

          stylesModified = 1;
        });

        // Remove style attribute if it's empty
        if (stylesModified && dom.getAttrib(node, 'style') === '') {
          node.removeAttribute('style');
          node.removeAttribute('data-mce-style');
        }

        // Remove attributes
        each(format.attributes, function (value, name) {
          var valueOut;

          value = FormatUtils.replaceVars(value, vars);

          // Indexed array
          if (typeof name === 'number') {
            name = value;
            compareNode = 0;
          }

          if (!compareNode || isEq(dom.getAttrib(compareNode, name), value)) {
            // Keep internal classes
            if (name === 'class') {
              value = dom.getAttrib(node, name);
              if (value) {
                // Build new class value where everything is removed except the internal prefixed classes
                valueOut = '';
                each(value.split(/\s+/), function (cls) {
                  if (/mce\-\w+/.test(cls)) {
                    valueOut += (valueOut ? ' ' : '') + cls;
                  }
                });

                // We got some internal classes left
                if (valueOut) {
                  dom.setAttrib(node, name, valueOut);
                  return;
                }
              }
            }

            // IE6 has a bug where the attribute doesn't get removed correctly
            if (name === "class") {
              node.removeAttribute('className');
            }

            // Remove mce prefixed attributes
            if (MCE_ATTR_RE.test(name)) {
              node.removeAttribute('data-mce-' + name);
            }

            node.removeAttribute(name);
          }
        });

        // Remove classes
        each(format.classes, function (value) {
          value = FormatUtils.replaceVars(value, vars);

          if (!compareNode || dom.hasClass(compareNode, value)) {
            dom.removeClass(node, value);
          }
        });

        // Check for non internal attributes
        attrs = dom.getAttribs(node);
        for (i = 0; i < attrs.length; i++) {
          var attrName = attrs[i].nodeName;
          if (attrName.indexOf('_') !== 0 && attrName.indexOf('data-') !== 0) {
            return false;
          }
        }
      }

      // Remove the inline child if it's empty for example <b> or <span>
      if (format.remove !== 'none') {
        removeNode(ed, node, format);
        return true;
      }
    };

    var findFormatRoot = function (editor, container, name, vars, similar) {
      var formatRoot;

      // Find format root
      each(FormatUtils.getParents(editor.dom, container.parentNode).reverse(), function (parent) {
        var format;

        // Find format root element
        if (!formatRoot && parent.id !== '_start' && parent.id !== '_end') {
          // Is the node matching the format we are looking for
          format = MatchFormat.matchNode(editor, parent, name, vars, similar);
          if (format && format.split !== false) {
            formatRoot = parent;
          }
        }
      });

      return formatRoot;
    };

    var wrapAndSplit = function (editor, formatList, formatRoot, container, target, split, format, vars) {
      var parent, clone, lastClone, firstClone, i, formatRootParent, dom = editor.dom;

      // Format root found then clone formats and split it
      if (formatRoot) {
        formatRootParent = formatRoot.parentNode;

        for (parent = container.parentNode; parent && parent !== formatRootParent; parent = parent.parentNode) {
          clone = dom.clone(parent, false);

          for (i = 0; i < formatList.length; i++) {
            if (removeFormat(editor, formatList[i], vars, clone, clone)) {
              clone = 0;
              break;
            }
          }

          // Build wrapper node
          if (clone) {
            if (lastClone) {
              clone.appendChild(lastClone);
            }

            if (!firstClone) {
              firstClone = clone;
            }

            lastClone = clone;
          }
        }

        // Never split block elements if the format is mixed
        if (split && (!format.mixed || !dom.isBlock(formatRoot))) {
          container = dom.split(formatRoot, container);
        }

        // Wrap container in cloned formats
        if (lastClone) {
          target.parentNode.insertBefore(lastClone, target);
          firstClone.appendChild(target);
        }
      }

      return container;
    };

    var remove = function (ed, name, vars, node, similar) {
      var formatList = ed.formatter.get(name), format = formatList[0];
      var bookmark, rng, contentEditable = true, dom = ed.dom, selection = ed.selection;

      var splitToFormatRoot = function (container) {
        var formatRoot = findFormatRoot(ed, container, name, vars, similar);
        return wrapAndSplit(ed, formatList, formatRoot, container, container, true, format, vars);
      };

      // Merges the styles for each node
      var process = function (node) {
        var children, i, l, lastContentEditable, hasContentEditableState;

        // Node has a contentEditable value
        if (node.nodeType === 1 && dom.getContentEditable(node)) {
          lastContentEditable = contentEditable;
          contentEditable = dom.getContentEditable(node) === "true";
          hasContentEditableState = true; // We don't want to wrap the container only it's children
        }

        // Grab the children first since the nodelist might be changed
        children = Tools.grep(node.childNodes);

        // Process current node
        if (contentEditable && !hasContentEditableState) {
          for (i = 0, l = formatList.length; i < l; i++) {
            if (removeFormat(ed, formatList[i], vars, node, node)) {
              break;
            }
          }
        }

        // Process the children
        if (format.deep) {
          if (children.length) {
            for (i = 0, l = children.length; i < l; i++) {
              process(children[i]);
            }

            if (hasContentEditableState) {
              contentEditable = lastContentEditable; // Restore last contentEditable state from stack
            }
          }
        }
      };

      var unwrap = function (start) {
        var node = dom.get(start ? '_start' : '_end'),
          out = node[start ? 'firstChild' : 'lastChild'];

        // If the end is placed within the start the result will be removed
        // So this checks if the out node is a bookmark node if it is it
        // checks for another more suitable node
        if (BookmarkManager.isBookmarkNode(out)) {
          out = out[start ? 'firstChild' : 'lastChild'];
        }

        // Since dom.remove removes empty text nodes then we need to try to find a better node
        if (out.nodeType === 3 && out.data.length === 0) {
          out = start ? node.previousSibling || node.nextSibling : node.nextSibling || node.previousSibling;
        }

        dom.remove(node, true);

        return out;
      };

      var removeRngStyle = function (rng) {
        var startContainer, endContainer;
        var commonAncestorContainer = rng.commonAncestorContainer;

        rng = ExpandRange.expandRng(ed, rng, formatList, true);

        if (format.split) {
          startContainer = getContainer(ed, rng, true);
          endContainer = getContainer(ed, rng);

          if (startContainer !== endContainer) {
            // WebKit will render the table incorrectly if we wrap a TH or TD in a SPAN
            // so let's see if we can use the first child instead
            // This will happen if you triple click a table cell and use remove formatting
            if (/^(TR|TH|TD)$/.test(startContainer.nodeName) && startContainer.firstChild) {
              if (startContainer.nodeName === "TR") {
                startContainer = startContainer.firstChild.firstChild || startContainer;
              } else {
                startContainer = startContainer.firstChild || startContainer;
              }
            }

            // Try to adjust endContainer as well if cells on the same row were selected - bug #6410
            if (commonAncestorContainer &&
              /^T(HEAD|BODY|FOOT|R)$/.test(commonAncestorContainer.nodeName) &&
              isTableCell(endContainer) && endContainer.firstChild) {
              endContainer = endContainer.firstChild || endContainer;
            }

            if (dom.isChildOf(startContainer, endContainer) && !dom.isBlock(endContainer) &&
              !isTableCell(startContainer) && !isTableCell(endContainer)) {
              startContainer = wrap(dom, startContainer, 'span', { id: '_start', 'data-mce-type': 'bookmark' });
              splitToFormatRoot(startContainer);
              startContainer = unwrap(true);
              return;
            }

            // Wrap start/end nodes in span element since these might be cloned/moved
            startContainer = wrap(dom, startContainer, 'span', { id: '_start', 'data-mce-type': 'bookmark' });
            endContainer = wrap(dom, endContainer, 'span', { id: '_end', 'data-mce-type': 'bookmark' });

            // Split start/end
            splitToFormatRoot(startContainer);
            splitToFormatRoot(endContainer);

            // Unwrap start/end to get real elements again
            startContainer = unwrap(true);
            endContainer = unwrap();
          } else {
            startContainer = endContainer = splitToFormatRoot(startContainer);
          }

          // Update range positions since they might have changed after the split operations
          rng.startContainer = startContainer.parentNode ? startContainer.parentNode : startContainer;
          rng.startOffset = dom.nodeIndex(startContainer);
          rng.endContainer = endContainer.parentNode ? endContainer.parentNode : endContainer;
          rng.endOffset = dom.nodeIndex(endContainer) + 1;
        }

        // Remove items between start/end
        new RangeUtils(dom).walk(rng, function (nodes) {
          each(nodes, function (node) {
            process(node);

            // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined.
            if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' &&
              node.parentNode && FormatUtils.getTextDecoration(dom, node.parentNode) === 'underline') {
              removeFormat(ed, {
                'deep': false,
                'exact': true,
                'inline': 'span',
                'styles': {
                  'textDecoration': 'underline'
                }
              }, null, node);
            }
          });
        });
      };

      // Handle node
      if (node) {
        if (node.nodeType) {
          rng = dom.createRng();
          rng.setStartBefore(node);
          rng.setEndAfter(node);
          removeRngStyle(rng);
        } else {
          removeRngStyle(node);
        }

        return;
      }

      if (dom.getContentEditable(selection.getNode()) === "false") {
        node = selection.getNode();
        for (var i = 0, l = formatList.length; i < l; i++) {
          if (formatList[i].ceFalseOverride) {
            if (removeFormat(ed, formatList[i], vars, node, node)) {
              break;
            }
          }
        }

        return;
      }

      if (!selection.isCollapsed() || !format.inline || dom.select('td[data-mce-selected],th[data-mce-selected]').length) {
        bookmark = selection.getBookmark();
        removeRngStyle(selection.getRng(true));
        selection.moveToBookmark(bookmark);

        // Check if start element still has formatting then we are at: "<b>text|</b>text"
        // and need to move the start into the next text node
        if (format.inline && MatchFormat.match(ed, name, vars, selection.getStart())) {
          FormatUtils.moveStart(dom, selection, selection.getRng(true));
        }

        ed.nodeChanged();
      } else {
        CaretFormat.removeCaretFormat(ed, name, vars, similar);
      }
    };

    return {
      removeFormat: removeFormat,
      remove: remove
    };
  }
);
/**
 * MergeFormats.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
 */

define(
  'tinymce.core.fmt.MergeFormats',
  [
    'ephox.katamari.api.Fun',
    'tinymce.core.dom.BookmarkManager',
    'tinymce.core.dom.ElementUtils',
    'tinymce.core.dom.NodeType',
    'tinymce.core.fmt.CaretFormat',
    'tinymce.core.fmt.FormatUtils',
    'tinymce.core.fmt.MatchFormat',
    'tinymce.core.fmt.RemoveFormat',
    'tinymce.core.util.Tools'
  ],
  function (Fun, BookmarkManager, ElementUtils, NodeType, CaretFormat, FormatUtils, MatchFormat, RemoveFormat, Tools) {
    var each = Tools.each;

    var isElementNode = function (node) {
      return node && node.nodeType === 1 && !BookmarkManager.isBookmarkNode(node) && !CaretFormat.isCaretNode(node) && !NodeType.isBogus(node);
    };

    var findElementSibling = function (node, siblingName) {
      var sibling;

      for (sibling = node; sibling; sibling = sibling[siblingName]) {
        if (sibling.nodeType === 3 && sibling.nodeValue.length !== 0) {
          return node;
        }

        if (sibling.nodeType === 1 && !BookmarkManager.isBookmarkNode(sibling)) {
          return sibling;
        }
      }

      return node;
    };

    var mergeSiblingsNodes = function (dom, prev, next) {
      var sibling, tmpSibling, elementUtils = new ElementUtils(dom);

      // Check if next/prev exists and that they are elements
      if (prev && next) {
        // If previous sibling is empty then jump over it
        prev = findElementSibling(prev, 'previousSibling');
        next = findElementSibling(next, 'nextSibling');

        // Compare next and previous nodes
        if (elementUtils.compare(prev, next)) {
          // Append nodes between
          for (sibling = prev.nextSibling; sibling && sibling !== next;) {
            tmpSibling = sibling;
            sibling = sibling.nextSibling;
            prev.appendChild(tmpSibling);
          }

          dom.remove(next);

          Tools.each(Tools.grep(next.childNodes), function (node) {
            prev.appendChild(node);
          });

          return prev;
        }
      }

      return next;
    };

    var processChildElements = function (node, filter, process) {
      each(node.childNodes, function (node) {
        if (isElementNode(node)) {
          if (filter(node)) {
            process(node);
          }
          if (node.hasChildNodes()) {
            processChildElements(node, filter, process);
          }
        }
      });
    };

    var hasStyle = function (dom, name) {
      return Fun.curry(function (name, node) {
        return !!(node && FormatUtils.getStyle(dom, node, name));
      }, name);
    };

    var applyStyle = function (dom, name, value) {
      return Fun.curry(function (name, value, node) {
        dom.setStyle(node, name, value);

        if (node.getAttribute('style') === '') {
          node.removeAttribute('style');
        }

        unwrapEmptySpan(dom, node);
      }, name, value);
    };

    var unwrapEmptySpan = function (dom, node) {
      if (node.nodeName === 'SPAN' && dom.getAttribs(node).length === 0) {
        dom.remove(node, true);
      }
    };

    var processUnderlineAndColor = function (dom, node) {
      var textDecoration;
      if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) {
        textDecoration = FormatUtils.getTextDecoration(dom, node.parentNode);
        if (dom.getStyle(node, 'color') && textDecoration) {
          dom.setStyle(node, 'text-decoration', textDecoration);
        } else if (dom.getStyle(node, 'text-decoration') === textDecoration) {
          dom.setStyle(node, 'text-decoration', null);
        }
      }
    };

    var mergeUnderlineAndColor = function (dom, format, vars, node) {
      // Colored nodes should be underlined so that the color of the underline matches the text color.
      if (format.styles.color || format.styles.textDecoration) {
        Tools.walk(node, Fun.curry(processUnderlineAndColor, dom), 'childNodes');
        processUnderlineAndColor(dom, node);
      }
    };

    var mergeBackgroundColorAndFontSize = function (dom, format, vars, node) {
      // nodes with font-size should have their own background color as well to fit the line-height (see TINY-882)
      if (format.styles && format.styles.backgroundColor) {
        processChildElements(node,
          hasStyle(dom, 'fontSize'),
          applyStyle(dom, 'backgroundColor', FormatUtils.replaceVars(format.styles.backgroundColor, vars))
        );
      }
    };

    var mergeSubSup = function (dom, format, vars, node) {
      // Remove font size on all chilren of a sub/sup and remove the inverse element
      if (format.inline === 'sub' || format.inline === 'sup') {
        processChildElements(node,
          hasStyle(dom, 'fontSize'),
          applyStyle(dom, 'fontSize', '')
        );

        dom.remove(dom.select(format.inline === 'sup' ? 'sub' : 'sup', node), true);
      }
    };

    var mergeSiblings = function (dom, format, vars, node) {
      // Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b>
      if (node && format.merge_siblings !== false) {
        node = mergeSiblingsNodes(dom, FormatUtils.getNonWhiteSpaceSibling(node), node);
        node = mergeSiblingsNodes(dom, node, FormatUtils.getNonWhiteSpaceSibling(node, true));
      }
    };

    var clearChildStyles = function (dom, format, node) {
      if (format.clear_child_styles) {
        var selector = format.links ? '*:not(a)' : '*';
        each(dom.select(selector, node), function (node) {
          if (isElementNode(node)) {
            each(format.styles, function (value, name) {
              dom.setStyle(node, name, '');
            });
          }
        });
      }
    };

    var mergeWithChildren = function (editor, formatList, vars, node) {
      // Remove/merge children
      each(formatList, function (format) {
        // Merge all children of similar type will move styles from child to parent
        // this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span>
        // will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span>
        each(editor.dom.select(format.inline, node), function (child) {
          if (!isElementNode(child)) {
            return;
          }

          RemoveFormat.removeFormat(editor, format, vars, child, format.exact ? child : null);
        });

        clearChildStyles(editor.dom, format, node);
      });
    };

    var mergeWithParents = function (editor, format, name, vars, node) {
      // Remove format if direct parent already has the same format
      if (MatchFormat.matchNode(editor, node.parentNode, name, vars)) {
        if (RemoveFormat.removeFormat(editor, format, vars, node)) {
          return;
        }
      }

      // Remove format if any ancestor already has the same format
      if (format.merge_with_parents) {
        editor.dom.getParent(node.parentNode, function (parent) {
          if (MatchFormat.matchNode(editor, parent, name, vars)) {
            RemoveFormat.removeFormat(editor, format, vars, node);
            return true;
          }
        });
      }
    };

    return {
      mergeWithChildren: mergeWithChildren,
      mergeUnderlineAndColor: mergeUnderlineAndColor,
      mergeBackgroundColorAndFontSize: mergeBackgroundColorAndFontSize,
      mergeSubSup: mergeSubSup,
      mergeSiblings: mergeSiblings,
      mergeWithParents: mergeWithParents
    };
  }
);
/**
 * ApplyFormat.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
 */

define(
  'tinymce.core.fmt.ApplyFormat',
  [
    'tinymce.core.dom.BookmarkManager',
    'tinymce.core.dom.NodeType',
    'tinymce.core.dom.RangeNormalizer',
    'tinymce.core.dom.RangeUtils',
    'tinymce.core.fmt.CaretFormat',
    'tinymce.core.fmt.ExpandRange',
    'tinymce.core.fmt.FormatUtils',
    'tinymce.core.fmt.Hooks',
    'tinymce.core.fmt.MatchFormat',
    'tinymce.core.fmt.MergeFormats',
    'tinymce.core.util.Tools'
  ],
  function (BookmarkManager, NodeType, RangeNormalizer, RangeUtils, CaretFormat, ExpandRange, FormatUtils, Hooks, MatchFormat, MergeFormats, Tools) {
    var each = Tools.each;

    var isElementNode = function (node) {
      return node && node.nodeType === 1 && !BookmarkManager.isBookmarkNode(node) && !CaretFormat.isCaretNode(node) && !NodeType.isBogus(node);
    };

    var processChildElements = function (node, filter, process) {
      each(node.childNodes, function (node) {
        if (isElementNode(node)) {
          if (filter(node)) {
            process(node);
          }
          if (node.hasChildNodes()) {
            processChildElements(node, filter, process);
          }
        }
      });
    };

    var applyFormat = function (ed, name, vars, node) {
      var formatList = ed.formatter.get(name), format = formatList[0], bookmark, rng, isCollapsed = !node && ed.selection.isCollapsed();
      var dom = ed.dom, selection = ed.selection;

      var setElementFormat = function (elm, fmt) {
        fmt = fmt || format;

        if (elm) {
          if (fmt.onformat) {
            fmt.onformat(elm, fmt, vars, node);
          }

          each(fmt.styles, function (value, name) {
            dom.setStyle(elm, name, FormatUtils.replaceVars(value, vars));
          });

          // Needed for the WebKit span spam bug
          // TODO: Remove this once WebKit/Blink fixes this
          if (fmt.styles) {
            var styleVal = dom.getAttrib(elm, 'style');

            if (styleVal) {
              elm.setAttribute('data-mce-style', styleVal);
            }
          }

          each(fmt.attributes, function (value, name) {
            dom.setAttrib(elm, name, FormatUtils.replaceVars(value, vars));
          });

          each(fmt.classes, function (value) {
            value = FormatUtils.replaceVars(value, vars);

            if (!dom.hasClass(elm, value)) {
              dom.addClass(elm, value);
            }
          });
        }
      };

      var applyNodeStyle = function (formatList, node) {
        var found = false;

        if (!format.selector) {
          return false;
        }

        // Look for matching formats
        each(formatList, function (format) {
          // Check collapsed state if it exists
          if ('collapsed' in format && format.collapsed !== isCollapsed) {
            return;
          }

          if (dom.is(node, format.selector) && !CaretFormat.isCaretNode(node)) {
            setElementFormat(node, format);
            found = true;
            return false;
          }
        });

        return found;
      };

      var applyRngStyle = function (dom, rng, bookmark, nodeSpecific) {
        var newWrappers = [], wrapName, wrapElm, contentEditable = true;

        // Setup wrapper element
        wrapName = format.inline || format.block;
        wrapElm = dom.create(wrapName);
        setElementFormat(wrapElm);

        new RangeUtils(dom).walk(rng, function (nodes) {
          var currentWrapElm;

          /**
           * Process a list of nodes wrap them.
           */
          var process = function (node) {
            var nodeName, parentName, hasContentEditableState, lastContentEditable;

            lastContentEditable = contentEditable;
            nodeName = node.nodeName.toLowerCase();
            parentName = node.parentNode.nodeName.toLowerCase();

            // Node has a contentEditable value
            if (node.nodeType === 1 && dom.getContentEditable(node)) {
              lastContentEditable = contentEditable;
              contentEditable = dom.getContentEditable(node) === "true";
              hasContentEditableState = true; // We don't want to wrap the container only it's children
            }

            // Stop wrapping on br elements
            if (FormatUtils.isEq(nodeName, 'br')) {
              currentWrapElm = 0;

              // Remove any br elements when we wrap things
              if (format.block) {
                dom.remove(node);
              }

              return;
            }

            // If node is wrapper type
            if (format.wrapper && MatchFormat.matchNode(ed, node, name, vars)) {
              currentWrapElm = 0;
              return;
            }

            // Can we rename the block
            // TODO: Break this if up, too complex
            if (contentEditable && !hasContentEditableState && format.block &&
              !format.wrapper && FormatUtils.isTextBlock(ed, nodeName) && FormatUtils.isValid(ed, parentName, wrapName)) {
              node = dom.rename(node, wrapName);
              setElementFormat(node);
              newWrappers.push(node);
              currentWrapElm = 0;
              return;
            }

            // Handle selector patterns
            if (format.selector) {
              var found = applyNodeStyle(formatList, node);

              // Continue processing if a selector match wasn't found and a inline element is defined
              if (!format.inline || found) {
                currentWrapElm = 0;
                return;
              }
            }

            // Is it valid to wrap this item
            // TODO: Break this if up, too complex
            if (contentEditable && !hasContentEditableState && FormatUtils.isValid(ed, wrapName, nodeName) && FormatUtils.isValid(ed, parentName, wrapName) &&
              !(!nodeSpecific && node.nodeType === 3 &&
                node.nodeValue.length === 1 &&
                node.nodeValue.charCodeAt(0) === 65279) &&
              !CaretFormat.isCaretNode(node) &&
              (!format.inline || !dom.isBlock(node))) {
              // Start wrapping
              if (!currentWrapElm) {
                // Wrap the node
                currentWrapElm = dom.clone(wrapElm, false);
                node.parentNode.insertBefore(currentWrapElm, node);
                newWrappers.push(currentWrapElm);
              }

              currentWrapElm.appendChild(node);
            } else {
              // Start a new wrapper for possible children
              currentWrapElm = 0;

              each(Tools.grep(node.childNodes), process);

              if (hasContentEditableState) {
                contentEditable = lastContentEditable; // Restore last contentEditable state from stack
              }

              // End the last wrapper
              currentWrapElm = 0;
            }
          };

          // Process siblings from range
          each(nodes, process);
        });

        // Apply formats to links as well to get the color of the underline to change as well
        if (format.links === true) {
          each(newWrappers, function (node) {
            var process = function (node) {
              if (node.nodeName === 'A') {
                setElementFormat(node, format);
              }

              each(Tools.grep(node.childNodes), process);
            };

            process(node);
          });
        }

        // Cleanup
        each(newWrappers, function (node) {
          var childCount;

          var getChildCount = function (node) {
            var count = 0;

            each(node.childNodes, function (node) {
              if (!FormatUtils.isWhiteSpaceNode(node) && !BookmarkManager.isBookmarkNode(node)) {
                count++;
              }
            });

            return count;
          };

          var getChildElementNode = function (root) {
            var child = false;
            each(root.childNodes, function (node) {
              if (isElementNode(node)) {
                child = node;
                return false; // break loop
              }
            });
            return child;
          };

          var mergeStyles = function (node) {
            var child, clone;

            child = getChildElementNode(node);

            // If child was found and of the same type as the current node
            if (child && !BookmarkManager.isBookmarkNode(child) && MatchFormat.matchName(dom, child, format)) {
              clone = dom.clone(child, false);
              setElementFormat(clone);

              dom.replace(clone, node, true);
              dom.remove(child, 1);
            }

            return clone || node;
          };

          childCount = getChildCount(node);

          // Remove empty nodes but only if there is multiple wrappers and they are not block
          // elements so never remove single <h1></h1> since that would remove the
          // current empty block element where the caret is at
          if ((newWrappers.length > 1 || !dom.isBlock(node)) && childCount === 0) {
            dom.remove(node, 1);
            return;
          }

          if (format.inline || format.wrapper) {
            // Merges the current node with it's children of similar type to reduce the number of elements
            if (!format.exact && childCount === 1) {
              node = mergeStyles(node);
            }

            MergeFormats.mergeWithChildren(ed, formatList, vars, node);
            MergeFormats.mergeWithParents(ed, format, name, vars, node);
            MergeFormats.mergeBackgroundColorAndFontSize(dom, format, vars, node);
            MergeFormats.mergeSubSup(dom, format, vars, node);
            MergeFormats.mergeSiblings(dom, format, vars, node);
          }
        });
      };

      if (dom.getContentEditable(selection.getNode()) === "false") {
        node = selection.getNode();
        for (var i = 0, l = formatList.length; i < l; i++) {
          if (formatList[i].ceFalseOverride && dom.is(node, formatList[i].selector)) {
            setElementFormat(node, formatList[i]);
            return;
          }
        }

        return;
      }

      if (format) {
        if (node) {
          if (node.nodeType) {
            if (!applyNodeStyle(formatList, node)) {
              rng = dom.createRng();
              rng.setStartBefore(node);
              rng.setEndAfter(node);
              applyRngStyle(dom, ExpandRange.expandRng(ed, rng, formatList), null, true);
            }
          } else {
            applyRngStyle(dom, node, null, true);
          }
        } else {
          if (!isCollapsed || !format.inline || dom.select('td[data-mce-selected],th[data-mce-selected]').length) {
            // Obtain selection node before selection is unselected by applyRngStyle
            var curSelNode = ed.selection.getNode();

            // If the formats have a default block and we can't find a parent block then
            // start wrapping it with a DIV this is for forced_root_blocks: false
            // It's kind of a hack but people should be using the default block type P since all desktop editors work that way
            if (!ed.settings.forced_root_block && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) {
              applyFormat(ed, formatList[0].defaultBlock);
            }

            // Apply formatting to selection
            ed.selection.setRng(RangeNormalizer.normalize(ed.selection.getRng()));
            bookmark = selection.getBookmark();
            applyRngStyle(dom, ExpandRange.expandRng(ed, selection.getRng(true), formatList), bookmark);

            if (format.styles) {
              MergeFormats.mergeUnderlineAndColor(dom, format, vars, curSelNode);
            }

            selection.moveToBookmark(bookmark);
            FormatUtils.moveStart(dom, selection, selection.getRng(true));
            ed.nodeChanged();
          } else {
            CaretFormat.applyCaretFormat(ed, name, vars);
          }
        }

        Hooks.postProcess(name, ed);
      }
    };

    return {
      applyFormat: applyFormat
    };
  }
);
/**
 * FormatChanged.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
 */

define(
  'tinymce.core.fmt.FormatChanged',
  [
    'ephox.katamari.api.Cell',
    'tinymce.core.fmt.FormatUtils',
    'tinymce.core.fmt.MatchFormat',
    'tinymce.core.util.Tools'
  ],
  function (Cell, FormatUtils, MatchFormat, Tools) {
    var each = Tools.each;

    var setup = function (formatChangeData, editor) {
      var currentFormats = {};

      formatChangeData.set({});

      editor.on('NodeChange', function (e) {
        var parents = FormatUtils.getParents(editor.dom, e.element), matchedFormats = {};

        // Ignore bogus nodes like the <a> tag created by moveStart()
        parents = Tools.grep(parents, function (node) {
          return node.nodeType === 1 && !node.getAttribute('data-mce-bogus');
        });

        // Check for new formats
        each(formatChangeData.get(), function (callbacks, format) {
          each(parents, function (node) {
            if (editor.formatter.matchNode(node, format, {}, callbacks.similar)) {
              if (!currentFormats[format]) {
                // Execute callbacks
                each(callbacks, function (callback) {
                  callback(true, { node: node, format: format, parents: parents });
                });

                currentFormats[format] = callbacks;
              }

              matchedFormats[format] = callbacks;
              return false;
            }

            if (MatchFormat.matchesUnInheritedFormatSelector(editor, node, format)) {
              return false;
            }
          });
        });

        // Check if current formats still match
        each(currentFormats, function (callbacks, format) {
          if (!matchedFormats[format]) {
            delete currentFormats[format];

            each(callbacks, function (callback) {
              callback(false, { node: e.element, format: format, parents: parents });
            });
          }
        });
      });
    };

    var addListeners = function (formatChangeData, formats, callback, similar) {
      var formatChangeItems = formatChangeData.get();

      each(formats.split(','), function (format) {
        if (!formatChangeItems[format]) {
          formatChangeItems[format] = [];
          formatChangeItems[format].similar = similar;
        }

        formatChangeItems[format].push(callback);
      });

      formatChangeData.set(formatChangeItems);
    };

    var formatChanged = function (editor, formatChangeState, formats, callback, similar) {
      if (formatChangeState.get() === null) {
        setup(formatChangeState, editor);
      }

      addListeners(formatChangeState, formats, callback, similar);
    };

    return {
      formatChanged: formatChanged
    };
  }
);
/**
 * DefaultFormats.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
 */

define(
  'tinymce.core.fmt.DefaultFormats',
  [
    'tinymce.core.util.Tools'
  ],
  function (Tools) {
    var get = function (dom) {
      var formats = {
        valigntop: [
          { selector: 'td,th', styles: { 'verticalAlign': 'top' } }
        ],

        valignmiddle: [
          { selector: 'td,th', styles: { 'verticalAlign': 'middle' } }
        ],

        valignbottom: [
          { selector: 'td,th', styles: { 'verticalAlign': 'bottom' } }
        ],

        alignleft: [
          {
            selector: 'figure.image',
            collapsed: false,
            classes: 'align-left',
            ceFalseOverride: true,
            preview: 'font-family font-size'
          },
          {
            selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li',
            styles: {
              textAlign: 'left'
            },
            inherit: false,
            preview: false,
            defaultBlock: 'div'
          },
          {
            selector: 'img,table',
            collapsed: false,
            styles: {
              'float': 'left'
            },
            preview: 'font-family font-size'
          }
        ],

        aligncenter: [
          {
            selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li',
            styles: {
              textAlign: 'center'
            },
            inherit: false,
            preview: 'font-family font-size',
            defaultBlock: 'div'
          },
          {
            selector: 'figure.image',
            collapsed: false,
            classes: 'align-center',
            ceFalseOverride: true,
            preview: 'font-family font-size'
          },
          {
            selector: 'img',
            collapsed: false,
            styles: {
              display: 'block',
              marginLeft: 'auto',
              marginRight: 'auto'
            },
            preview: false
          },
          {
            selector: 'table',
            collapsed: false,
            styles: {
              marginLeft: 'auto',
              marginRight: 'auto'
            },
            preview: 'font-family font-size'
          }
        ],

        alignright: [
          {
            selector: 'figure.image',
            collapsed: false,
            classes: 'align-right',
            ceFalseOverride: true,
            preview: 'font-family font-size'
          },
          {
            selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li',
            styles: {
              textAlign: 'right'
            },
            inherit: false,
            preview: 'font-family font-size',
            defaultBlock: 'div'
          },
          {
            selector: 'img,table',
            collapsed: false,
            styles: {
              'float': 'right'
            },
            preview: 'font-family font-size'
          }
        ],

        alignjustify: [
          {
            selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li',
            styles: {
              textAlign: 'justify'
            },
            inherit: false,
            defaultBlock: 'div',
            preview: 'font-family font-size'
          }
        ],

        bold: [
          { inline: 'strong', remove: 'all' },
          { inline: 'span', styles: { fontWeight: 'bold' } },
          { inline: 'b', remove: 'all' }
        ],

        italic: [
          { inline: 'em', remove: 'all' },
          { inline: 'span', styles: { fontStyle: 'italic' } },
          { inline: 'i', remove: 'all' }
        ],

        underline: [
          { inline: 'span', styles: { textDecoration: 'underline' }, exact: true },
          { inline: 'u', remove: 'all' }
        ],

        strikethrough: [
          { inline: 'span', styles: { textDecoration: 'line-through' }, exact: true },
          { inline: 'strike', remove: 'all' }
        ],

        forecolor: { inline: 'span', styles: { color: '%value' }, links: true, remove_similar: true, clear_child_styles: true },
        hilitecolor: { inline: 'span', styles: { backgroundColor: '%value' }, links: true, remove_similar: true, clear_child_styles: true },
        fontname: { inline: 'span', styles: { fontFamily: '%value' }, clear_child_styles: true },
        fontsize: { inline: 'span', styles: { fontSize: '%value' }, clear_child_styles: true },
        fontsize_class: { inline: 'span', attributes: { 'class': '%value' } },
        blockquote: { block: 'blockquote', wrapper: 1, remove: 'all' },
        subscript: { inline: 'sub' },
        superscript: { inline: 'sup' },
        code: { inline: 'code' },

        link: {
          inline: 'a', selector: 'a', remove: 'all', split: true, deep: true,
          onmatch: function () {
            return true;
          },

          onformat: function (elm, fmt, vars) {
            Tools.each(vars, function (value, key) {
              dom.setAttrib(elm, key, value);
            });
          }
        },

        removeformat: [
          {
            selector: 'b,strong,em,i,font,u,strike,sub,sup,dfn,code,samp,kbd,var,cite,mark,q,del,ins',
            remove: 'all',
            split: true,
            expand: false,
            block_expand: true,
            deep: true
          },
          { selector: 'span', attributes: ['style', 'class'], remove: 'empty', split: true, expand: false, deep: true },
          { selector: '*', attributes: ['style', 'class'], split: false, expand: false, deep: true }
        ]
      };

      Tools.each('p h1 h2 h3 h4 h5 h6 div address pre div dt dd samp'.split(/\s/), function (name) {
        formats[name] = { block: name, remove: 'all' };
      });

      return formats;
    };

    return {
      get: get
    };
  }
);
/**
 * FormatRegistry.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
 */

define(
  'tinymce.core.fmt.FormatRegistry',
  [
    'tinymce.core.fmt.DefaultFormats',
    'tinymce.core.util.Tools'
  ],
  function (DefaultFormats, Tools) {
    return function (editor) {
      var formats = {};

      var get = function (name) {
        return name ? formats[name] : formats;
      };

      var register = function (name, format) {
        if (name) {
          if (typeof name !== 'string') {
            Tools.each(name, function (format, name) {
              register(name, format);
            });
          } else {
            // Force format into array and add it to internal collection
            format = format.length ? format : [format];

            Tools.each(format, function (format) {
              // Set deep to false by default on selector formats this to avoid removing
              // alignment on images inside paragraphs when alignment is changed on paragraphs
              if (typeof format.deep === 'undefined') {
                format.deep = !format.selector;
              }

              // Default to true
              if (typeof format.split === 'undefined') {
                format.split = !format.selector || format.inline;
              }

              // Default to true
              if (typeof format.remove === 'undefined' && format.selector && !format.inline) {
                format.remove = 'none';
              }

              // Mark format as a mixed format inline + block level
              if (format.selector && format.inline) {
                format.mixed = true;
                format.block_expand = true;
              }

              // Split classes if needed
              if (typeof format.classes === 'string') {
                format.classes = format.classes.split(/\s+/);
              }
            });

            formats[name] = format;
          }
        }
      };

      var unregister = function (name) {
        if (name && formats[name]) {
          delete formats[name];
        }

        return formats;
      };

      register(DefaultFormats.get(editor.dom));
      register(editor.settings.formats);

      return {
        get: get,
        register: register,
        unregister: unregister
      };
    };
  }
);

/**
 * Preview.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
 */

/**
 * Internal class for generating previews styles for formats.
 *
 * Example:
 *  Preview.getCssText(editor, 'bold');
 *
 * @private
 * @class tinymce.fmt.Preview
 */
define(
  'tinymce.core.fmt.Preview',
  [
    "tinymce.core.dom.DOMUtils",
    "tinymce.core.util.Tools",
    "tinymce.core.html.Schema"
  ],
  function (DOMUtils, Tools, Schema) {
    var each = Tools.each;
    var dom = DOMUtils.DOM;

    var parsedSelectorToHtml = function (ancestry, editor) {
      var elm, item, fragment;
      var schema = editor && editor.schema || new Schema({});

      var decorate = function (elm, item) {
        if (item.classes.length) {
          dom.addClass(elm, item.classes.join(' '));
        }
        dom.setAttribs(elm, item.attrs);
      };

      var createElement = function (sItem) {
        var elm;

        item = typeof sItem === 'string' ? {
          name: sItem,
          classes: [],
          attrs: {}
        } : sItem;

        elm = dom.create(item.name);
        decorate(elm, item);
        return elm;
      };

      var getRequiredParent = function (elm, candidate) {
        var name = typeof elm !== 'string' ? elm.nodeName.toLowerCase() : elm;
        var elmRule = schema.getElementRule(name);
        var parentsRequired = elmRule && elmRule.parentsRequired;

        if (parentsRequired && parentsRequired.length) {
          return candidate && Tools.inArray(parentsRequired, candidate) !== -1 ? candidate : parentsRequired[0];
        } else {
          return false;
        }
      };

      var wrapInHtml = function (elm, ancestry, siblings) {
        var parent, parentCandidate, parentRequired;
        var ancestor = ancestry.length > 0 && ancestry[0];
        var ancestorName = ancestor && ancestor.name;

        parentRequired = getRequiredParent(elm, ancestorName);

        if (parentRequired) {
          if (ancestorName === parentRequired) {
            parentCandidate = ancestry[0];
            ancestry = ancestry.slice(1);
          } else {
            parentCandidate = parentRequired;
          }
        } else if (ancestor) {
          parentCandidate = ancestry[0];
          ancestry = ancestry.slice(1);
        } else if (!siblings) {
          return elm;
        }

        if (parentCandidate) {
          parent = createElement(parentCandidate);
          parent.appendChild(elm);
        }

        if (siblings) {
          if (!parent) {
            // if no more ancestry, wrap in generic div
            parent = dom.create('div');
            parent.appendChild(elm);
          }

          Tools.each(siblings, function (sibling) {
            var siblingElm = createElement(sibling);
            parent.insertBefore(siblingElm, elm);
          });
        }

        return wrapInHtml(parent, ancestry, parentCandidate && parentCandidate.siblings);
      };

      if (ancestry && ancestry.length) {
        item = ancestry[0];
        elm = createElement(item);
        fragment = dom.create('div');
        fragment.appendChild(wrapInHtml(elm, ancestry.slice(1), item.siblings));
        return fragment;
      } else {
        return '';
      }
    };

    var selectorToHtml = function (selector, editor) {
      return parsedSelectorToHtml(parseSelector(selector), editor);
    };

    var parseSelectorItem = function (item) {
      var tagName;
      var obj = {
        classes: [],
        attrs: {}
      };

      item = obj.selector = Tools.trim(item);

      if (item !== '*') {
        // matching IDs, CLASSes, ATTRIBUTES and PSEUDOs
        tagName = item.replace(/(?:([#\.]|::?)([\w\-]+)|(\[)([^\]]+)\]?)/g, function ($0, $1, $2, $3, $4) {
          switch ($1) {
            case '#':
              obj.attrs.id = $2;
              break;

            case '.':
              obj.classes.push($2);
              break;

            case ':':
              if (Tools.inArray('checked disabled enabled read-only required'.split(' '), $2) !== -1) {
                obj.attrs[$2] = $2;
              }
              break;
          }

          // atribute matched
          if ($3 === '[') {
            var m = $4.match(/([\w\-]+)(?:\=\"([^\"]+))?/);
            if (m) {
              obj.attrs[m[1]] = m[2];
            }
          }

          return '';
        });
      }

      obj.name = tagName || 'div';
      return obj;
    };

    var parseSelector = function (selector) {
      if (!selector || typeof selector !== 'string') {
        return [];
      }

      // take into account only first one
      selector = selector.split(/\s*,\s*/)[0];

      // tighten
      selector = selector.replace(/\s*(~\+|~|\+|>)\s*/g, '$1');

      // split either on > or on space, but not the one inside brackets
      return Tools.map(selector.split(/(?:>|\s+(?![^\[\]]+\]))/), function (item) {
        // process each sibling selector separately
        var siblings = Tools.map(item.split(/(?:~\+|~|\+)/), parseSelectorItem);
        var obj = siblings.pop(); // the last one is our real target

        if (siblings.length) {
          obj.siblings = siblings;
        }
        return obj;
      }).reverse();
    };

    var getCssText = function (editor, format) {
      var name, previewFrag, previewElm, items;
      var previewCss = '', parentFontSize, previewStyles;

      previewStyles = editor.settings.preview_styles;

      // No preview forced
      if (previewStyles === false) {
        return '';
      }

      // Default preview
      if (typeof previewStyles !== 'string') {
        previewStyles = 'font-family font-size font-weight font-style text-decoration ' +
          'text-transform color background-color border border-radius outline text-shadow';
      }

      // Removes any variables since these can't be previewed
      var removeVars = function (val) {
        return val.replace(/%(\w+)/g, '');
      };

      // Create block/inline element to use for preview
      if (typeof format === "string") {
        format = editor.formatter.get(format);
        if (!format) {
          return;
        }

        format = format[0];
      }

      // Format specific preview override
      // TODO: This should probably be further reduced by the previewStyles option
      if ('preview' in format) {
        previewStyles = format.preview;
        if (previewStyles === false) {
          return '';
        }
      }

      name = format.block || format.inline || 'span';

      items = parseSelector(format.selector);
      if (items.length) {
        if (!items[0].name) { // e.g. something like ul > .someClass was provided
          items[0].name = name;
        }
        name = format.selector;
        previewFrag = parsedSelectorToHtml(items, editor);
      } else {
        previewFrag = parsedSelectorToHtml([name], editor);
      }

      previewElm = dom.select(name, previewFrag)[0] || previewFrag.firstChild;

      // Add format styles to preview element
      each(format.styles, function (value, name) {
        value = removeVars(value);

        if (value) {
          dom.setStyle(previewElm, name, value);
        }
      });

      // Add attributes to preview element
      each(format.attributes, function (value, name) {
        value = removeVars(value);

        if (value) {
          dom.setAttrib(previewElm, name, value);
        }
      });

      // Add classes to preview element
      each(format.classes, function (value) {
        value = removeVars(value);

        if (!dom.hasClass(previewElm, value)) {
          dom.addClass(previewElm, value);
        }
      });

      editor.fire('PreviewFormats');

      // Add the previewElm outside the visual area
      dom.setStyles(previewFrag, { position: 'absolute', left: -0xFFFF });
      editor.getBody().appendChild(previewFrag);

      // Get parent container font size so we can compute px values out of em/% for older IE:s
      parentFontSize = dom.getStyle(editor.getBody(), 'fontSize', true);
      parentFontSize = /px$/.test(parentFontSize) ? parseInt(parentFontSize, 10) : 0;

      each(previewStyles.split(' '), function (name) {
        var value = dom.getStyle(previewElm, name, true);

        // If background is transparent then check if the body has a background color we can use
        if (name === 'background-color' && /transparent|rgba\s*\([^)]+,\s*0\)/.test(value)) {
          value = dom.getStyle(editor.getBody(), name, true);

          // Ignore white since it's the default color, not the nicest fix
          // TODO: Fix this by detecting runtime style
          if (dom.toHex(value).toLowerCase() === '#ffffff') {
            return;
          }
        }

        if (name === 'color') {
          // Ignore black since it's the default color, not the nicest fix
          // TODO: Fix this by detecting runtime style
          if (dom.toHex(value).toLowerCase() === '#000000') {
            return;
          }
        }

        // Old IE won't calculate the font size so we need to do that manually
        if (name === 'font-size') {
          if (/em|%$/.test(value)) {
            if (parentFontSize === 0) {
              return;
            }

            // Convert font size from em/% to px
            value = parseFloat(value, 10) / (/%$/.test(value) ? 100 : 1);
            value = (value * parentFontSize) + 'px';
          }
        }

        if (name === "border" && value) {
          previewCss += 'padding:0 2px;';
        }

        previewCss += name + ':' + value + ';';
      });

      editor.fire('AfterPreviewFormats');

      //previewCss += 'line-height:normal';

      dom.remove(previewFrag);

      return previewCss;
    };

    return {
      getCssText: getCssText,
      parseSelector: parseSelector,
      selectorToHtml: selectorToHtml
    };
  }
);

/**
 * ToggleFormat.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
 */

define(
  'tinymce.core.fmt.ToggleFormat',
  [
    'tinymce.core.fmt.ApplyFormat',
    'tinymce.core.fmt.MatchFormat',
    'tinymce.core.fmt.RemoveFormat'
  ],
  function (ApplyFormat, MatchFormat, RemoveFormat) {
    var toggle = function (editor, formats, name, vars, node) {
      var fmt = formats.get(name);

      if (MatchFormat.match(editor, name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) {
        RemoveFormat.remove(editor, name, vars, node);
      } else {
        ApplyFormat.applyFormat(editor, name, vars, node);
      }
    };

    return {
      toggle: toggle
    };
  }
);

/**
 * FormatShortcuts.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
 */

define(
  'tinymce.core.keyboard.FormatShortcuts',
  [
  ],
  function () {
    var setup = function (editor) {
      // Add some inline shortcuts
      editor.addShortcut('meta+b', '', 'Bold');
      editor.addShortcut('meta+i', '', 'Italic');
      editor.addShortcut('meta+u', '', 'Underline');

      // BlockFormat shortcuts keys
      for (var i = 1; i <= 6; i++) {
        editor.addShortcut('access+' + i, '', ['FormatBlock', false, 'h' + i]);
      }

      editor.addShortcut('access+7', '', ['FormatBlock', false, 'p']);
      editor.addShortcut('access+8', '', ['FormatBlock', false, 'div']);
      editor.addShortcut('access+9', '', ['FormatBlock', false, 'address']);
    };

    return {
      setup: setup
    };
  }
);

/**
 * Formatter.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
 */

/**
 * Text formatter engine class. This class is used to apply formats like bold, italic, font size
 * etc to the current selection or specific nodes. This engine was built to replace the browser's
 * default formatting logic for execCommand due to its inconsistent and buggy behavior.
 *
 * @class tinymce.Formatter
 * @example
 *  tinymce.activeEditor.formatter.register('mycustomformat', {
 *    inline: 'span',
 *    styles: {color: '#ff0000'}
 *  });
 *
 *  tinymce.activeEditor.formatter.apply('mycustomformat');
 */
define(
  'tinymce.core.api.Formatter',
  [
    'ephox.katamari.api.Cell',
    'ephox.katamari.api.Fun',
    'tinymce.core.fmt.ApplyFormat',
    'tinymce.core.fmt.FormatChanged',
    'tinymce.core.fmt.FormatRegistry',
    'tinymce.core.fmt.MatchFormat',
    'tinymce.core.fmt.Preview',
    'tinymce.core.fmt.RemoveFormat',
    'tinymce.core.fmt.ToggleFormat',
    'tinymce.core.keyboard.FormatShortcuts'
  ],
  function (Cell, Fun, ApplyFormat, FormatChanged, FormatRegistry, MatchFormat, Preview, RemoveFormat, ToggleFormat, FormatShortcuts) {
    /**
     * Constructs a new formatter instance.
     *
     * @constructor Formatter
     * @param {tinymce.Editor} ed Editor instance to construct the formatter engine to.
     */
    return function (editor) {
      var formats = FormatRegistry(editor);
      var formatChangeState = Cell(null);

      FormatShortcuts.setup(editor);

      return {
        /**
         * Returns the format by name or all formats if no name is specified.
         *
         * @method get
         * @param {String} name Optional name to retrieve by.
         * @return {Array/Object} Array/Object with all registered formats or a specific format.
         */
        get: formats.get,

        /**
         * Registers a specific format by name.
         *
         * @method register
         * @param {Object/String} name Name of the format for example "bold".
         * @param {Object/Array} format Optional format object or array of format variants
         * can only be omitted if the first arg is an object.
         */
        register: formats.register,

        /**
         * Unregister a specific format by name.
         *
         * @method unregister
         * @param {String} name Name of the format for example "bold".
         */
        unregister: formats.unregister,

        /**
         * Applies the specified format to the current selection or specified node.
         *
         * @method apply
         * @param {String} name Name of format to apply.
         * @param {Object} vars Optional list of variables to replace within format before applying it.
         * @param {Node} node Optional node to apply the format to defaults to current selection.
         */
        apply: Fun.curry(ApplyFormat.applyFormat, editor),

        /**
         * Removes the specified format from the current selection or specified node.
         *
         * @method remove
         * @param {String} name Name of format to remove.
         * @param {Object} vars Optional list of variables to replace within format before removing it.
         * @param {Node/Range} node Optional node or DOM range to remove the format from defaults to current selection.
         */
        remove: Fun.curry(RemoveFormat.remove, editor),

        /**
         * Toggles the specified format on/off.
         *
         * @method toggle
         * @param {String} name Name of format to apply/remove.
         * @param {Object} vars Optional list of variables to replace within format before applying/removing it.
         * @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection.
         */
        toggle: Fun.curry(ToggleFormat.toggle, editor, formats),

        /**
         * Matches the current selection or specified node against the specified format name.
         *
         * @method match
         * @param {String} name Name of format to match.
         * @param {Object} vars Optional list of variables to replace before checking it.
         * @param {Node} node Optional node to check.
         * @return {boolean} true/false if the specified selection/node matches the format.
         */
        match: Fun.curry(MatchFormat.match, editor),

        /**
         * Matches the current selection against the array of formats and returns a new array with matching formats.
         *
         * @method matchAll
         * @param {Array} names Name of format to match.
         * @param {Object} vars Optional list of variables to replace before checking it.
         * @return {Array} Array with matched formats.
         */
        matchAll: Fun.curry(MatchFormat.matchAll, editor),

        /**
         * Return true/false if the specified node has the specified format.
         *
         * @method matchNode
         * @param {Node} node Node to check the format on.
         * @param {String} name Format name to check.
         * @param {Object} vars Optional list of variables to replace before checking it.
         * @param {Boolean} similar Match format that has similar properties.
         * @return {Object} Returns the format object it matches or undefined if it doesn't match.
         */
        matchNode: Fun.curry(MatchFormat.matchNode, editor),

        /**
         * Returns true/false if the specified format can be applied to the current selection or not. It
         * will currently only check the state for selector formats, it returns true on all other format types.
         *
         * @method canApply
         * @param {String} name Name of format to check.
         * @return {boolean} true/false if the specified format can be applied to the current selection/node.
         */
        canApply: Fun.curry(MatchFormat.canApply, editor),

        /**
         * Executes the specified callback when the current selection matches the formats or not.
         *
         * @method formatChanged
         * @param {String} formats Comma separated list of formats to check for.
         * @param {function} callback Callback with state and args when the format is changed/toggled on/off.
         * @param {Boolean} similar True/false state if the match should handle similar or exact formats.
         */
        formatChanged: Fun.curry(FormatChanged.formatChanged, editor, formatChangeState),

        /**
         * Returns a preview css text for the specified format.
         *
         * @method getCssText
         * @param {String/Object} format Format to generate preview css text for.
         * @return {String} Css text for the specified format.
         * @example
         * var cssText1 = editor.formatter.getCssText('bold');
         * var cssText2 = editor.formatter.getCssText({inline: 'b'});
         */
        getCssText: Fun.curry(Preview.getCssText, editor)
      };
    };
  }
);

define(
  'ephox.sugar.api.properties.Attr',

  [
    'ephox.katamari.api.Type',
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Obj',
    'ephox.sugar.api.node.Node',
    'global!Error',
    'global!console'
  ],

  /*
   * Direct attribute manipulation has been around since IE8, but
   * was apparently unstable until IE10.
   */
  function (Type, Arr, Obj, Node, Error, console) {
    var rawSet = function (dom, key, value) {
      /*
       * JQuery coerced everything to a string, and silently did nothing on text node/null/undefined.
       *
       * We fail on those invalid cases, only allowing numbers and booleans.
       */
      if (Type.isString(value) || Type.isBoolean(value) || Type.isNumber(value)) {
        dom.setAttribute(key, value + '');
      } else {
        console.error('Invalid call to Attr.set. Key ', key, ':: Value ', value, ':: Element ', dom);
        throw new Error('Attribute value was not simple');
      }
    };

    var set = function (element, key, value) {
      rawSet(element.dom(), key, value);
    };

    var setAll = function (element, attrs) {
      var dom = element.dom();
      Obj.each(attrs, function (v, k) {
        rawSet(dom, k, v);
      });
    };

    var get = function (element, key) {
      var v = element.dom().getAttribute(key);

      // undefined is the more appropriate value for JS, and this matches JQuery
      return v === null ? undefined : v;
    };

    var has = function (element, key) {
      var dom = element.dom();

      // return false for non-element nodes, no point in throwing an error
      return dom && dom.hasAttribute ? dom.hasAttribute(key) : false;
    };

    var remove = function (element, key) {
      element.dom().removeAttribute(key);
    };

    var hasNone = function (element) {
      var attrs = element.dom().attributes;
      return attrs === undefined || attrs === null || attrs.length === 0;
    };

    var clone = function (element) {
      return Arr.foldl(element.dom().attributes, function (acc, attr) {
        acc[attr.name] = attr.value;
        return acc;
      }, {});
    };

    var transferOne = function (source, destination, attr) {
      // NOTE: We don't want to clobber any existing attributes
      if (has(source, attr) && !has(destination, attr)) set(destination, attr, get(source, attr));        
    };

    // Transfer attributes(attrs) from source to destination, unless they are already present
    var transfer = function (source, destination, attrs) {
      if (!Node.isElement(source) || !Node.isElement(destination)) return;
      Arr.each(attrs, function (attr) {
        transferOne(source, destination, attr);
      });
    };

    return {
      clone: clone,
      set: set,
      setAll: setAll,
      get: get,
      has: has,
      remove: remove,
      hasNone: hasNone,
      transfer: transfer
    };
  }
);

define(
  'ephox.sugar.impl.Style',

  [

  ],

  function () {
    // some elements, such as mathml, don't have style attributes
    var isSupported = function (dom) {
      return dom.style !== undefined;
    };

    return {
      isSupported: isSupported
    };
  }
);
define(
  'ephox.sugar.api.properties.Css',

  [
    'ephox.katamari.api.Type',
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Obj',
    'ephox.katamari.api.Option',
    'ephox.sugar.api.properties.Attr',
    'ephox.sugar.api.node.Body',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.node.Node',
    'ephox.sugar.impl.Style',
    'ephox.katamari.api.Strings',
    'global!Error',
    'global!console',
    'global!window'
  ],

  function (Type, Arr, Obj, Option, Attr, Body, Element, Node, Style, Strings, Error, console, window) {
    var internalSet = function (dom, property, value) {
      // This is going to hurt. Apologies.
      // JQuery coerces numbers to pixels for certain property names, and other times lets numbers through.
      // we're going to be explicit; strings only.
      if (!Type.isString(value)) {
        console.error('Invalid call to CSS.set. Property ', property, ':: Value ', value, ':: Element ', dom);
        throw new Error('CSS value must be a string: ' + value);
      }

      // removed: support for dom().style[property] where prop is camel case instead of normal property name
      if (Style.isSupported(dom)) dom.style.setProperty(property, value);
    };

    var internalRemove = function (dom, property) {
      /*
       * IE9 and above - MDN doesn't have details, but here's a couple of random internet claims
       *
       * http://help.dottoro.com/ljopsjck.php
       * http://stackoverflow.com/a/7901886/7546
       */
      if (Style.isSupported(dom)) dom.style.removeProperty(property);
    };

    var set = function (element, property, value) {
      var dom = element.dom();
      internalSet(dom, property, value);
    };

    var setAll = function (element, css) {
      var dom = element.dom();

      Obj.each(css, function (v, k) {
        internalSet(dom, k, v);
      });
    };

    var setOptions = function(element, css) {
      var dom = element.dom();

      Obj.each(css, function (v, k) {
        v.fold(function () {
          internalRemove(dom, k);
        }, function (value) {
          internalSet(dom, k, value);
        });
      });
    };

    /*
     * NOTE: For certain properties, this returns the "used value" which is subtly different to the "computed value" (despite calling getComputedStyle).
     * Blame CSS 2.0.
     *
     * https://developer.mozilla.org/en-US/docs/Web/CSS/used_value
     */
    var get = function (element, property) {
      var dom = element.dom();
      /*
       * IE9 and above per
       * https://developer.mozilla.org/en/docs/Web/API/window.getComputedStyle
       *
       * Not in numerosity, because it doesn't memoize and looking this up dynamically in performance critical code would be horrendous.
       *
       * JQuery has some magic here for IE popups, but we don't really need that.
       * It also uses element.ownerDocument.defaultView to handle iframes but that hasn't been required since FF 3.6.
       */
      var styles = window.getComputedStyle(dom);
      var r = styles.getPropertyValue(property);

      // jquery-ism: If r is an empty string, check that the element is not in a document. If it isn't, return the raw value.
      // Turns out we do this a lot.
      var v = (r === '' && !Body.inBody(element)) ? getUnsafeProperty(dom, property) : r;

      // undefined is the more appropriate value for JS. JQuery coerces to an empty string, but screw that!
      return v === null ? undefined : v;
    };

    var getUnsafeProperty = function (dom, property) {
      // removed: support for dom().style[property] where prop is camel case instead of normal property name
      // empty string is what the browsers (IE11 and Chrome) return when the propertyValue doesn't exists.
      return Style.isSupported(dom) ? dom.style.getPropertyValue(property) : '';
    };

    /*
     * Gets the raw value from the style attribute. Useful for retrieving "used values" from the DOM:
     * https://developer.mozilla.org/en-US/docs/Web/CSS/used_value
     *
     * Returns NONE if the property isn't set, or the value is an empty string.
     */
    var getRaw = function (element, property) {
      var dom = element.dom();
      var raw = getUnsafeProperty(dom, property);

      return Option.from(raw).filter(function (r) { return r.length > 0; });
    };

    var isValidValue = function (tag, property, value) {
      var element = Element.fromTag(tag);
      set(element, property, value);
      var style = getRaw(element, property);
      return style.isSome();
    };

    var remove = function (element, property) {
      var dom = element.dom();

      internalRemove(dom, property);

      if (Attr.has(element, 'style') && Strings.trim(Attr.get(element, 'style')) === '') {
        // No more styles left, remove the style attribute as well
        Attr.remove(element, 'style');
      }
    };

    var preserve = function (element, f) {
      var oldStyles = Attr.get(element, 'style');
      var result = f(element);
      var restore = oldStyles === undefined ? Attr.remove : Attr.set;
      restore(element, 'style', oldStyles);
      return result;
    };

    var copy = function (source, target) {
      var sourceDom = source.dom();
      var targetDom = target.dom();
      if (Style.isSupported(sourceDom) && Style.isSupported(targetDom)) {
        targetDom.style.cssText = sourceDom.style.cssText;
      }
    };

    var reflow = function (e) {
      /* NOTE:
       * do not rely on this return value.
       * It's here so the closure compiler doesn't optimise the property access away.
       */
      return e.dom().offsetWidth;
    };

    var transferOne = function (source, destination, style) {
      getRaw(source, style).each(function (value) {
        // NOTE: We don't want to clobber any existing inline styles.
        if (getRaw(destination, style).isNone()) set(destination, style, value);
      });
    };

    var transfer = function (source, destination, styles) {
      if (!Node.isElement(source) || !Node.isElement(destination)) return;
      Arr.each(styles, function (style) {
        transferOne(source, destination, style);
      });
    };

    return {
      copy: copy,
      set: set,
      preserve: preserve,
      setAll: setAll,
      setOptions: setOptions,
      remove: remove,
      get: get,
      getRaw: getRaw,
      isValidValue: isValidValue,
      reflow: reflow,
      transfer: transfer
    };
  }
);

/**
 * EditorView.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

define(
  'tinymce.core.EditorView',
  [
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.properties.Css',
    'ephox.sugar.api.search.Traverse'
  ],
  function (Fun, Option, Compare, Element, Css, Traverse) {
    var getProp = function (propName, elm) {
      var rawElm = elm.dom();
      return rawElm[propName];
    };

    var getComputedSizeProp = function (propName, elm) {
      return parseInt(Css.get(elm, propName), 10);
    };

    var getClientWidth = Fun.curry(getProp, 'clientWidth');
    var getClientHeight = Fun.curry(getProp, 'clientHeight');
    var getMarginTop = Fun.curry(getComputedSizeProp, 'margin-top');
    var getMarginLeft = Fun.curry(getComputedSizeProp, 'margin-left');

    var getBoundingClientRect = function (elm) {
      return elm.dom().getBoundingClientRect();
    };

    var isInsideElementContentArea = function (bodyElm, clientX, clientY) {
      var clientWidth = getClientWidth(bodyElm);
      var clientHeight = getClientHeight(bodyElm);

      return clientX >= 0 && clientY >= 0 && clientX <= clientWidth && clientY <= clientHeight;
    };

    var transpose = function (inline, elm, clientX, clientY) {
      var clientRect = getBoundingClientRect(elm);
      var deltaX = inline ? clientRect.left + elm.dom().clientLeft + getMarginLeft(elm) : 0;
      var deltaY = inline ? clientRect.top + elm.dom().clientTop + getMarginTop(elm) : 0;
      var x = clientX - deltaX;
      var y = clientY - deltaY;

      return { x: x, y: y };
    };

    // Checks if the specified coordinate is within the visual content area excluding the scrollbars
    var isXYInContentArea = function (editor, clientX, clientY) {
      var bodyElm = Element.fromDom(editor.getBody());
      var targetElm = editor.inline ? bodyElm : Traverse.documentElement(bodyElm);
      var transposedPoint = transpose(editor.inline, targetElm, clientX, clientY);

      return isInsideElementContentArea(targetElm, transposedPoint.x, transposedPoint.y);
    };

    var fromDomSafe = function (node) {
      return Option.from(node).map(Element.fromDom);
    };

    var isEditorAttachedToDom = function (editor) {
      var rawContainer = editor.inline ? editor.getBody() : editor.getContentAreaContainer();

      return fromDomSafe(rawContainer).map(function (container) {
        return Compare.contains(Traverse.owner(container), container);
      }).getOr(false);
    };

    return {
      isXYInContentArea: isXYInContentArea,
      isEditorAttachedToDom: isEditorAttachedToDom
    };
  }
);

/**
 * NotificationManagerImpl.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
 */

define(
  'tinymce.core.ui.NotificationManagerImpl',
  [
  ],
  function () {
    return function () {
      var unimplemented = function () {
        throw new Error('Theme did not provide a NotificationManager implementation.');
      };

      return {
        open: unimplemented,
        close: unimplemented,
        reposition: unimplemented,
        getArgs: unimplemented
      };
    };
  }
);

/**
 * NotificationManager.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
 */

/**
 * This class handles the creation of TinyMCE's notifications.
 *
 * @class tinymce.NotificationManager
 * @example
 * // Opens a new notification of type "error" with text "An error occurred."
 * tinymce.activeEditor.notificationManager.open({
 *    text: 'An error occurred.',
 *    type: 'error'
 * });
 */
define(
  'tinymce.core.api.NotificationManager',
  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Option',
    'tinymce.core.EditorView',
    'tinymce.core.ui.NotificationManagerImpl',
    'tinymce.core.util.Delay'
  ],
  function (Arr, Option, EditorView, NotificationManagerImpl, Delay) {
    return function (editor) {
      var notifications = [];

      var getImplementation = function () {
        var theme = editor.theme;
        return theme && theme.getNotificationManagerImpl ? theme.getNotificationManagerImpl() : NotificationManagerImpl();
      };

      var getTopNotification = function () {
        return Option.from(notifications[0]);
      };

      var isEqual = function (a, b) {
        return a.type === b.type && a.text === b.text && !a.progressBar && !a.timeout && !b.progressBar && !b.timeout;
      };

      var reposition = function () {
        if (notifications.length > 0) {
          getImplementation().reposition(notifications);
        }
      };

      var addNotification = function (notification) {
        notifications.push(notification);
      };

      var closeNotification = function (notification) {
        Arr.findIndex(notifications, function (otherNotification) {
          return otherNotification === notification;
        }).each(function (index) {
          // Mutate here since third party might have stored away the window array
          // TODO: Consider breaking this api
          notifications.splice(index, 1);
        });
      };

      var open = function (args) {
        // Never open notification if editor has been removed.
        if (editor.removed || !EditorView.isEditorAttachedToDom(editor)) {
          return;
        }

        return Arr.find(notifications, function (notification) {
          return isEqual(getImplementation().getArgs(notification), args);
        }).getOrThunk(function () {
          editor.editorManager.setActive(editor);

          var notification = getImplementation().open(args, function () {
            closeNotification(notification);
            reposition();
          });

          addNotification(notification);
          reposition();
          return notification;
        });
      };

      var close = function () {
        getTopNotification().each(function (notification) {
          getImplementation().close(notification);
          closeNotification(notification);
          reposition();
        });
      };

      var getNotifications = function () {
        return notifications;
      };

      var registerEvents = function (editor) {
        editor.on('SkinLoaded', function () {
          var serviceMessage = editor.settings.service_message;

          if (serviceMessage) {
            open({
              text: serviceMessage,
              type: 'warning',
              timeout: 0,
              icon: ''
            });
          }
        });

        editor.on('ResizeEditor ResizeWindow', function () {
          Delay.requestAnimationFrame(reposition);
        });

        editor.on('remove', function () {
          Arr.each(notifications, function (notification) {
            getImplementation().close(notification);
          });
        });
      };

      registerEvents(editor);

      return {
        /**
         * Opens a new notification.
         *
         * @method open
         * @param {Object} args Optional name/value settings collection contains things like timeout/color/message etc.
         */
        open: open,

        /**
         * Closes the top most notification.
         *
         * @method close
         */
        close: close,

        /**
         * Returns the currently opened notification objects.
         *
         * @method getNotifications
         * @return {Array} Array of the currently opened notifications.
         */
        getNotifications: getNotifications
      };
    };
  }
);

define(
  'ephox.katamari.api.Adt',

  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Obj',
    'ephox.katamari.api.Type',
    'global!Array',
    'global!Error',
    'global!console'
  ],

  function (Arr, Obj, Type, Array, Error, console) {
    /*
     * Generates a church encoded ADT (https://en.wikipedia.org/wiki/Church_encoding)
     * For syntax and use, look at the test code.
     */
    var generate = function (cases) {
      // validation
      if (!Type.isArray(cases)) {
        throw new Error('cases must be an array');
      }
      if (cases.length === 0) {
        throw new Error('there must be at least one case');
      }

      var constructors = [ ];

      // adt is mutated to add the individual cases
      var adt = {};
      Arr.each(cases, function (acase, count) {
        var keys = Obj.keys(acase);

        // validation
        if (keys.length !== 1) {
          throw new Error('one and only one name per case');
        }

        var key = keys[0];
        var value = acase[key];

        // validation
        if (adt[key] !== undefined) {
          throw new Error('duplicate key detected:' + key);
        } else if (key === 'cata') {
          throw new Error('cannot have a case named cata (sorry)');
        } else if (!Type.isArray(value)) {
          // this implicitly checks if acase is an object
          throw new Error('case arguments must be an array');
        }

        constructors.push(key);
        //
        // constructor for key
        //
        adt[key] = function () {
          var argLength = arguments.length;

          // validation
          if (argLength !== value.length) {
            throw new Error('Wrong number of arguments to case ' + key + '. Expected ' + value.length + ' (' + value + '), got ' + argLength);
          }

          // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome
          var args = new Array(argLength);
          for (var i = 0; i < args.length; i++) args[i] = arguments[i];


          var match = function (branches) {
            var branchKeys = Obj.keys(branches);
            if (constructors.length !== branchKeys.length) {
              throw new Error('Wrong number of arguments to match. Expected: ' + constructors.join(',') + '\nActual: ' + branchKeys.join(','));
            }

            var allReqd = Arr.forall(constructors, function (reqKey) {
              return Arr.contains(branchKeys, reqKey);
            });

            if (!allReqd) throw new Error('Not all branches were specified when using match. Specified: ' + branchKeys.join(', ') + '\nRequired: ' + constructors.join(', '));

            return branches[key].apply(null, args);
          };

          //
          // the fold function for key
          //
          return {
            fold: function (/* arguments */) {
              // runtime validation
              if (arguments.length !== cases.length) {
                throw new Error('Wrong number of arguments to fold. Expected ' + cases.length + ', got ' + arguments.length);
              }
              var target = arguments[count];
              return target.apply(null, args);
            },
            match: match,

            // NOTE: Only for debugging.
            log: function (label) {
              console.log(label, {
                constructors: constructors,
                constructor: key,
                params: args
              });
            }
          };
        };
      });

      return adt;
    };
    return {
      generate: generate
    };
  }
);
define(
  'ephox.sugar.api.selection.Selection',

  [
    'ephox.katamari.api.Adt',
    'ephox.katamari.api.Struct'
  ],

  function (Adt, Struct) {
    // Consider adding a type for "element"
    var type = Adt.generate([
      { domRange: [ 'rng' ] },
      { relative: [ 'startSitu', 'finishSitu' ] },
      { exact: [ 'start', 'soffset', 'finish', 'foffset' ] }
    ]);

    var range = Struct.immutable(
      'start',
      'soffset',
      'finish',
      'foffset'
    );

    var exactFromRange = function (simRange) {
      return type.exact(simRange.start(), simRange.soffset(), simRange.finish(), simRange.foffset());
    };

    return {
      domRange: type.domRange,
      relative: type.relative,
      exact: type.exact,

      exactFromRange: exactFromRange,
      range: range
    };
  }
);

define(
  'ephox.sugar.api.dom.DocumentPosition',

  [
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.search.Traverse'
  ],

  function (Compare, Element, Traverse) {
    var makeRange = function (start, soffset, finish, foffset) {
      var doc = Traverse.owner(start);

      // TODO: We need to think about a better place to put native range creation code. Does it even belong in sugar?
      // Could the `Compare` checks (node.compareDocumentPosition) handle these situations better?
      var rng = doc.dom().createRange();
      rng.setStart(start.dom(), soffset);
      rng.setEnd(finish.dom(), foffset);
      return rng;
    };

    // Return the deepest - or furthest down the document tree - Node that contains both boundary points
    // of the range (start:soffset, finish:foffset).
    var commonAncestorContainer = function (start, soffset, finish, foffset) {
      var r = makeRange(start, soffset, finish, foffset);
      return Element.fromDom(r.commonAncestorContainer);
    };

    var after = function (start, soffset, finish, foffset) {
      var r = makeRange(start, soffset, finish, foffset);

      var same = Compare.eq(start, finish) && soffset === foffset;
      return r.collapsed && !same;
    };

    return {
      after: after,
      commonAncestorContainer: commonAncestorContainer
    };
  }
);
define(
  'ephox.sugar.api.node.Fragment',

  [
    'ephox.katamari.api.Arr',
    'ephox.sugar.api.node.Element',
    'global!document'
  ],

  function (Arr, Element, document) {
    var fromElements = function (elements, scope) {
      var doc = scope || document;
      var fragment = doc.createDocumentFragment();
      Arr.each(elements, function (element) {
        fragment.appendChild(element.dom());
      });
      return Element.fromDom(fragment);
    };

    return {
      fromElements: fromElements
    };
  }
);

define(
  'ephox.sugar.api.selection.Situ',

  [
    'ephox.katamari.api.Adt'
  ],

  function (Adt) {
    var adt = Adt.generate([
      { 'before': [ 'element' ] },
      { 'on': [ 'element', 'offset' ] },
      { after: [ 'element' ] }
    ]);

    // Probably don't need this given that we now have "match"
    var cata = function (subject, onBefore, onOn, onAfter) {
      return subject.fold(onBefore, onOn, onAfter);
    };

    return {
      before: adt.before,
      on: adt.on,
      after: adt.after,
      cata: cata
    };
  }
);

define(
  'ephox.sugar.selection.core.NativeRange',

  [
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.node.Element'
  ],

  function (Fun, Option, Compare, Element) {
    var selectNodeContents = function (win, element) {
      var rng = win.document.createRange();
      selectNodeContentsUsing(rng, element);
      return rng;
    };

    var selectNodeContentsUsing = function (rng, element) {
      rng.selectNodeContents(element.dom());
    };

    var isWithin = function (outerRange, innerRange) {
      // Adapted from: http://stackoverflow.com/questions/5605401/insert-link-in-contenteditable-element
      return innerRange.compareBoundaryPoints(outerRange.END_TO_START, outerRange) < 1 &&
        innerRange.compareBoundaryPoints(outerRange.START_TO_END, outerRange) > -1;
    };

    var create = function (win) {
      return win.document.createRange();
    };

    // NOTE: Mutates the range.
    var setStart = function (rng, situ) {
      situ.fold(function (e) {
        rng.setStartBefore(e.dom());
      }, function (e, o) {
        rng.setStart(e.dom(), o);
      }, function (e) {
        rng.setStartAfter(e.dom());
      });
    };

    var setFinish = function (rng, situ) {
      situ.fold(function (e) {
        rng.setEndBefore(e.dom());
      }, function (e, o) {
        rng.setEnd(e.dom(), o);
      }, function (e) {
        rng.setEndAfter(e.dom());
      });
    };

    var replaceWith = function (rng, fragment) {
      // Note: this document fragment approach may not work on IE9.
      deleteContents(rng);
      rng.insertNode(fragment.dom());
    };

    var isCollapsed = function (start, soffset, finish, foffset) {
      return Compare.eq(start, finish) && soffset === foffset;
    };

    var relativeToNative = function (win, startSitu, finishSitu) {
      var range = win.document.createRange();
      setStart(range, startSitu);
      setFinish(range, finishSitu);
      return range;
    };

    var exactToNative = function (win, start, soffset, finish, foffset) {
      var rng = win.document.createRange();
      rng.setStart(start.dom(), soffset);
      rng.setEnd(finish.dom(), foffset);
      return rng;
    };

    var deleteContents = function (rng) {
      rng.deleteContents();
    };

    var cloneFragment = function (rng) {
      var fragment = rng.cloneContents();
      return Element.fromDom(fragment);
    };

    var toRect = function (rect) {
      return {
        left: Fun.constant(rect.left),
        top: Fun.constant(rect.top),
        right: Fun.constant(rect.right),
        bottom: Fun.constant(rect.bottom),
        width: Fun.constant(rect.width),
        height: Fun.constant(rect.height)
      };
    };
    
    var getFirstRect = function (rng) {
      var rects = rng.getClientRects();
      // ASSUMPTION: The first rectangle is the start of the selection
      var rect = rects.length > 0 ? rects[0] : rng.getBoundingClientRect();
      return rect.width > 0 || rect.height > 0  ? Option.some(rect).map(toRect) : Option.none();
    };

    var getBounds = function (rng) {
      var rect = rng.getBoundingClientRect();
      return rect.width > 0 || rect.height > 0  ? Option.some(rect).map(toRect) : Option.none();
    };

    var toString = function (rng) {
      return rng.toString();
    };

    return {
      create: create,
      replaceWith: replaceWith,
      selectNodeContents: selectNodeContents,
      selectNodeContentsUsing: selectNodeContentsUsing,
      isCollapsed: isCollapsed,
      relativeToNative: relativeToNative,
      exactToNative: exactToNative,
      deleteContents: deleteContents,
      cloneFragment: cloneFragment,
      getFirstRect: getFirstRect,
      getBounds: getBounds,
      isWithin: isWithin,
      toString: toString
    };
  }
);

define(
  'ephox.sugar.selection.core.SelectionDirection',

  [
    'ephox.katamari.api.Adt',
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'ephox.katamari.api.Thunk',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.selection.core.NativeRange'
  ],

  function (Adt, Fun, Option, Thunk, Element, NativeRange) {
    var adt = Adt.generate([
      { ltr: [ 'start', 'soffset', 'finish', 'foffset' ] },
      { rtl: [ 'start', 'soffset', 'finish', 'foffset' ] }
    ]);

    var fromRange = function (win, type, range) {
      return type(Element.fromDom(range.startContainer), range.startOffset, Element.fromDom(range.endContainer), range.endOffset);
    };

    var getRanges = function (win, selection) {
      return selection.match({
        domRange: function (rng) {
          return {
            ltr: Fun.constant(rng),
            rtl: Option.none
          };
        },
        relative: function (startSitu, finishSitu) {
          return {
            ltr: Thunk.cached(function () {
              return NativeRange.relativeToNative(win, startSitu, finishSitu);
            }),
            rtl: Thunk.cached(function () {
              return Option.some(
                NativeRange.relativeToNative(win, finishSitu, startSitu)
              );
            })
          };
        },
        exact: function (start, soffset, finish, foffset) {
          return {
            ltr: Thunk.cached(function () {
              return NativeRange.exactToNative(win, start, soffset, finish, foffset);
            }),
            rtl: Thunk.cached(function () {
              return Option.some(
                NativeRange.exactToNative(win, finish, foffset, start, soffset)
              );
            })
          };
        }
      });
    };

    var doDiagnose = function (win, ranges) {
      // If we cannot create a ranged selection from start > finish, it could be RTL
      var rng = ranges.ltr();
      if (rng.collapsed) {
        // Let's check if it's RTL ... if it is, then reversing the direction will not be collapsed
        var reversed = ranges.rtl().filter(function (rev) {
          return rev.collapsed === false;
        });
        
        return reversed.map(function (rev) {
          // We need to use "reversed" here, because the original only has one point (collapsed)
          return adt.rtl(
            Element.fromDom(rev.endContainer), rev.endOffset,
            Element.fromDom(rev.startContainer), rev.startOffset  
          );
        }).getOrThunk(function () {
          return fromRange(win, adt.ltr, rng);
        });
      } else {
        return fromRange(win, adt.ltr, rng);
      }
    };

    var diagnose = function (win, selection) {
      var ranges = getRanges(win, selection);
      return doDiagnose(win, ranges);
    };

    var asLtrRange = function (win, selection) {
      var diagnosis = diagnose(win, selection);
      return diagnosis.match({
        ltr: function (start, soffset, finish, foffset) {
          var rng = win.document.createRange();
          rng.setStart(start.dom(), soffset);
          rng.setEnd(finish.dom(), foffset);
          return rng;
        },
        rtl: function (start, soffset, finish, foffset) {
          // NOTE: Reversing start and finish
          var rng = win.document.createRange();
          rng.setStart(finish.dom(), foffset);
          rng.setEnd(start.dom(), soffset);
          return rng;
        }
      });
    };

    return {
      ltr: adt.ltr,
      rtl: adt.rtl,
      diagnose: diagnose,
      asLtrRange: asLtrRange
    };
  }
);

define(
  'ephox.katamari.api.Options',

  [
    'ephox.katamari.api.Option'
  ],

  function (Option) {
    /** cat :: [Option a] -> [a] */
    var cat = function (arr) {
      var r = [];
      var push = function (x) {
        r.push(x);
      };
      for (var i = 0; i < arr.length; i++) {
        arr[i].each(push);
      }
      return r;
    };

    /** findMap :: ([a], (a, Int -> Option b)) -> Option b */
    var findMap = function (arr, f) {
      for (var i = 0; i < arr.length; i++) {
        var r = f(arr[i], i);
        if (r.isSome()) {
          return r;
        }
      }
      return Option.none();
    };

    /**
     * if all elements in arr are 'some', their inner values are passed as arguments to f
     * f must have arity arr.length
    */
    var liftN = function(arr, f) {
      var r = [];
      for (var i = 0; i < arr.length; i++) {
        var x = arr[i];
        if (x.isSome()) {
          r.push(x.getOrDie());
        } else {
          return Option.none();
        }
      }
      return Option.some(f.apply(null, r));
    };

    return {
      cat: cat,
      findMap: findMap,
      liftN: liftN
    };
  }
);

defineGlobal("global!Math", Math);
define(
  'ephox.sugar.selection.alien.Geometry',

  [
    'global!Math'
  ],

  function (Math) {
    var searchForPoint = function (rectForOffset, x, y, maxX, length) {
      // easy cases
      if (length === 0) return 0;
      else if (x === maxX) return length - 1;

      var xDelta = maxX;

      // start at 1, zero is the fallback
      for (var i = 1; i < length; i++) {
        var rect = rectForOffset(i);
        var curDeltaX = Math.abs(x - rect.left);

        if (y > rect.bottom) {
          // range is too high, above drop point, do nothing
        } else if (y < rect.top || curDeltaX > xDelta) {
          // if the search winds up on the line below the drop point,
          // or we pass the best X offset,
          // wind back to the previous (best) delta
          return i - 1;
        } else {
          // update current search delta
          xDelta = curDeltaX;
        }
      }
      return 0; // always return something, even if it's not the exact offset it'll be better than nothing
    };

    var inRect = function (rect, x, y) {
      return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
    };

    return {
      inRect: inRect,
      searchForPoint: searchForPoint
    };

  }
);

define(
  'ephox.sugar.selection.query.TextPoint',

  [
    'ephox.katamari.api.Option',
    'ephox.katamari.api.Options',
    'ephox.sugar.api.node.Text',
    'ephox.sugar.selection.alien.Geometry',
    'global!Math'
  ],

  function (Option, Options, Text, Geometry, Math) {
    var locateOffset = function (doc, textnode, x, y, rect) {
      var rangeForOffset = function (offset) {
        var r = doc.dom().createRange();
        r.setStart(textnode.dom(), offset);
        r.collapse(true);
        return r;
      };

      var rectForOffset = function (offset) {
        var r = rangeForOffset(offset);
        return r.getBoundingClientRect();
      };

      var length = Text.get(textnode).length;
      var offset = Geometry.searchForPoint(rectForOffset, x, y, rect.right, length);
      return rangeForOffset(offset);
    };

    var locate = function (doc, node, x, y) {
      var r = doc.dom().createRange();
      r.selectNode(node.dom());
      var rects = r.getClientRects();
      var foundRect = Options.findMap(rects, function (rect) {
        return Geometry.inRect(rect, x, y) ? Option.some(rect) : Option.none();
      });

      return foundRect.map(function (rect) {
        return locateOffset(doc, node, x, y, rect);
      });
    };

    return {
      locate: locate
    };
  }
);

define(
  'ephox.sugar.selection.query.ContainerPoint',

  [
    'ephox.katamari.api.Option',
    'ephox.katamari.api.Options',
    'ephox.sugar.api.node.Node',
    'ephox.sugar.api.search.Traverse',
    'ephox.sugar.selection.alien.Geometry',
    'ephox.sugar.selection.query.TextPoint',
    'global!Math'
  ],

  function (Option, Options, Node, Traverse, Geometry, TextPoint, Math) {
    /**
     * Future idea:
     *
     * This code requires the drop point to be contained within the nodes array somewhere. If it isn't,
     * we fall back to the extreme start or end of the node array contents.
     * This isn't really what the user intended.
     *
     * In theory, we could just find the range point closest to the boxes representing the node
     * (repartee does something similar).
     */

    var searchInChildren = function (doc, node, x, y) {
      var r = doc.dom().createRange();
      var nodes = Traverse.children(node);
      return Options.findMap(nodes, function (n) {
        // slight mutation because we assume creating ranges is expensive
        r.selectNode(n.dom());
        return Geometry.inRect(r.getBoundingClientRect(), x, y) ?
                locateNode(doc, n, x, y) :
                Option.none();
      });
    };

    var locateNode = function (doc, node, x, y) {
      var locator = Node.isText(node) ? TextPoint.locate : searchInChildren;
      return locator(doc, node, x, y);
    };

    var locate = function (doc, node, x, y) {
      var r = doc.dom().createRange();
      r.selectNode(node.dom());
      var rect = r.getBoundingClientRect();
      // Clamp x,y at the bounds of the node so that the locate function has SOME chance
      var boundedX = Math.max(rect.left, Math.min(rect.right, x));
      var boundedY = Math.max(rect.top, Math.min(rect.bottom, y));

      return locateNode(doc, node, boundedX, boundedY);
    };

    return {
      locate: locate
    };
  }
);

define(
  'ephox.sugar.impl.ClosestOrAncestor',

  [
    'ephox.katamari.api.Type',
    'ephox.katamari.api.Option'
  ],

  function (Type, Option) {
    return function (is, ancestor, scope, a, isRoot) {
      return is(scope, a) ?
              Option.some(scope) :
              Type.isFunction(isRoot) && isRoot(scope) ?
                  Option.none() :
                  ancestor(scope, a, isRoot);
    };
  }
);
define(
  'ephox.sugar.api.search.PredicateFind',

  [
    'ephox.katamari.api.Type',
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'ephox.sugar.api.node.Body',
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.impl.ClosestOrAncestor'
  ],

  function (Type, Arr, Fun, Option, Body, Compare, Element, ClosestOrAncestor) {
    var first = function (predicate) {
      return descendant(Body.body(), predicate);
    };

    var ancestor = function (scope, predicate, isRoot) {
      var element = scope.dom();
      var stop = Type.isFunction(isRoot) ? isRoot : Fun.constant(false);

      while (element.parentNode) {
        element = element.parentNode;
        var el = Element.fromDom(element);

        if (predicate(el)) return Option.some(el);
        else if (stop(el)) break;
      }
      return Option.none();
    };

    var closest = function (scope, predicate, isRoot) {
      // This is required to avoid ClosestOrAncestor passing the predicate to itself
      var is = function (scope) {
        return predicate(scope);
      };
      return ClosestOrAncestor(is, ancestor, scope, predicate, isRoot);
    };

    var sibling = function (scope, predicate) {
      var element = scope.dom();
      if (!element.parentNode) return Option.none();

      return child(Element.fromDom(element.parentNode), function (x) {
        return !Compare.eq(scope, x) && predicate(x);
      });
    };

    var child = function (scope, predicate) {
      var result = Arr.find(scope.dom().childNodes,
        Fun.compose(predicate, Element.fromDom));
      return result.map(Element.fromDom);
    };

    var descendant = function (scope, predicate) {
      var descend = function (element) {
        for (var i = 0; i < element.childNodes.length; i++) {
          if (predicate(Element.fromDom(element.childNodes[i])))
            return Option.some(Element.fromDom(element.childNodes[i]));

          var res = descend(element.childNodes[i]);
          if (res.isSome())
            return res;
        }

        return Option.none();
      };

      return descend(scope.dom());
    };

    return {
      first: first,
      ancestor: ancestor,
      closest: closest,
      sibling: sibling,
      child: child,
      descendant: descendant
    };
  }
);

define(
  'ephox.sugar.api.selection.Awareness',

  [
    'ephox.katamari.api.Arr',
    'ephox.sugar.api.node.Node',
    'ephox.sugar.api.node.Text',
    'ephox.sugar.api.search.Traverse'
  ],

  function (Arr, Node, Text, Traverse) {
    var getEnd = function (element) {
      return Node.name(element) === 'img' ? 1 : Text.getOption(element).fold(function () {
        return Traverse.children(element).length;
      }, function (v) {
        return v.length;
      });
    };

    var isEnd = function (element, offset) {
      return getEnd(element) === offset;
    };

    var isStart = function (element, offset) {
      return offset === 0;
    };

    var NBSP = '\u00A0';

    var isTextNodeWithCursorPosition = function (el) {
      return Text.getOption(el).filter(function (text) {
        // For the purposes of finding cursor positions only allow text nodes with content,
        // but trim removes &nbsp; and that's allowed
        return text.trim().length !== 0 || text.indexOf(NBSP) > -1;
      }).isSome();
    };

    var elementsWithCursorPosition = [ 'img', 'br' ];
    var isCursorPosition = function (elem) {
      var hasCursorPosition = isTextNodeWithCursorPosition(elem);
      return hasCursorPosition || Arr.contains(elementsWithCursorPosition, Node.name(elem));
    };

    return {
      getEnd: getEnd,
      isEnd: isEnd,
      isStart: isStart,
      isCursorPosition: isCursorPosition
    };
  }
);

define(
  'ephox.sugar.api.selection.CursorPosition',

  [
    'ephox.katamari.api.Option',
    'ephox.sugar.api.search.PredicateFind',
    'ephox.sugar.api.search.Traverse',
    'ephox.sugar.api.selection.Awareness'
  ],

  function (Option, PredicateFind, Traverse, Awareness) {
    var first = function (element) {
      return PredicateFind.descendant(element, Awareness.isCursorPosition);
    };

    var last = function (element) {
      return descendantRtl(element, Awareness.isCursorPosition);
    };

    // Note, sugar probably needs some RTL traversals.
    var descendantRtl = function (scope, predicate) {
      var descend = function (element) {
        var children = Traverse.children(element);
        for (var i = children.length - 1; i >= 0; i--) {
          var child = children[i];
          if (predicate(child)) return Option.some(child);
          var res = descend(child);
          if (res.isSome()) return res;
        }

        return Option.none();
      };

      return descend(scope);
    };

    return {
      first: first,
      last: last
    };
  }
);

define(
  'ephox.sugar.selection.query.EdgePoint',

  [
    'ephox.katamari.api.Option',
    'ephox.sugar.api.search.Traverse',
    'ephox.sugar.api.selection.CursorPosition'
  ],

  function (Option, Traverse, CursorPosition) {
    /* 
     * When a node has children, we return either the first or the last cursor
     * position, whichever is closer horizontally
     * 
     * When a node has no children, we return the start of end of the element,
     * depending on which is closer horizontally
     * */

    // TODO: Make this RTL compatible
    var COLLAPSE_TO_LEFT = true;
    var COLLAPSE_TO_RIGHT = false;

    var getCollapseDirection = function (rect, x) {
      return x - rect.left < rect.right - x ? COLLAPSE_TO_LEFT : COLLAPSE_TO_RIGHT;
    };

    var createCollapsedNode = function (doc, target, collapseDirection) {
      var r = doc.dom().createRange();
      r.selectNode(target.dom());
      r.collapse(collapseDirection);
      return r;
    };

    var locateInElement = function (doc, node, x) {
      var cursorRange = doc.dom().createRange();
      cursorRange.selectNode(node.dom());
      var rect = cursorRange.getBoundingClientRect();
      var collapseDirection = getCollapseDirection(rect, x);

      var f = collapseDirection === COLLAPSE_TO_LEFT ? CursorPosition.first : CursorPosition.last;
      return f(node).map(function (target) {
        return createCollapsedNode(doc, target, collapseDirection);
      });
    };

    var locateInEmpty = function (doc, node, x) {
      var rect = node.dom().getBoundingClientRect();
      var collapseDirection = getCollapseDirection(rect, x);
      return Option.some(createCollapsedNode(doc, node, collapseDirection));
    };

    var search = function (doc, node, x) {
      var f = Traverse.children(node).length === 0 ? locateInEmpty : locateInElement;
      return f(doc, node, x);
    };

    return {
      search: search
    };
  }
);

define(
  'ephox.sugar.selection.query.CaretRange',

  [
    'ephox.katamari.api.Option',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.search.Traverse',
    'ephox.sugar.api.selection.Selection',
    'ephox.sugar.selection.query.ContainerPoint',
    'ephox.sugar.selection.query.EdgePoint',
    'global!document',
    'global!Math'
  ],

  function (Option, Element, Traverse, Selection, ContainerPoint, EdgePoint, document, Math) {
    var caretPositionFromPoint = function (doc, x, y) {
      return Option.from(doc.dom().caretPositionFromPoint(x, y)).bind(function (pos) {
        // It turns out that Firefox can return null for pos.offsetNode
        if (pos.offsetNode === null) return Option.none();
        var r = doc.dom().createRange();
        r.setStart(pos.offsetNode, pos.offset);
        r.collapse();
        return Option.some(r);
      });
    };

    var caretRangeFromPoint = function (doc, x, y) {
      return Option.from(doc.dom().caretRangeFromPoint(x, y));
    };

    var searchTextNodes = function (doc, node, x, y) {
      var r = doc.dom().createRange();
      r.selectNode(node.dom());
      var rect = r.getBoundingClientRect();
      // Clamp x,y at the bounds of the node so that the locate function has SOME chance
      var boundedX = Math.max(rect.left, Math.min(rect.right, x));
      var boundedY = Math.max(rect.top, Math.min(rect.bottom, y));

      return ContainerPoint.locate(doc, node, boundedX, boundedY);
    };

    var searchFromPoint = function (doc, x, y) {
      // elementFromPoint is defined to return null when there is no element at the point
      // This often happens when using IE10 event.y instead of event.clientY
      return Option.from(doc.dom().elementFromPoint(x, y)).map(Element.fromDom).bind(function (elem) {
        // used when the x,y position points to an image, or outside the bounds
        var fallback = function () {
          return EdgePoint.search(doc, elem, x);
        };

        return Traverse.children(elem).length === 0 ? fallback() :
                // if we have children, search for the right text node and then get the offset out of it
                searchTextNodes(doc, elem, x, y).orThunk(fallback);
      });
    };

    var availableSearch = document.caretPositionFromPoint ? caretPositionFromPoint :  // defined standard
                          document.caretRangeFromPoint ? caretRangeFromPoint :        // webkit implementation
                          searchFromPoint;                                            // fallback


    var fromPoint = function (win, x, y) {
      var doc = Element.fromDom(win.document);
      return availableSearch(doc, x, y).map(function (rng) {
        return Selection.range(
          Element.fromDom(rng.startContainer),
          rng.startOffset,
          Element.fromDom(rng.endContainer),
          rng.endOffset
        );
      });
    };

    return {
      fromPoint: fromPoint
    };
  }
);

define(
  'ephox.sugar.selection.query.Within',

  [
    'ephox.katamari.api.Arr',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.node.Node',
    'ephox.sugar.api.search.SelectorFilter',
    'ephox.sugar.api.search.Selectors',
    'ephox.sugar.selection.core.NativeRange',
    'ephox.sugar.selection.core.SelectionDirection'
  ],

  function (Arr, Element, Node, SelectorFilter, Selectors, NativeRange, SelectionDirection) {
    var withinContainer = function (win, ancestor, outerRange, selector) {
      var innerRange = NativeRange.create(win);
      var self = Selectors.is(ancestor, selector) ? [ ancestor ] : [];
      var elements = self.concat(SelectorFilter.descendants(ancestor, selector));
      return Arr.filter(elements, function (elem) {
        // Mutate the selection to save creating new ranges each time
        NativeRange.selectNodeContentsUsing(innerRange, elem);
        return NativeRange.isWithin(outerRange, innerRange);
      });
    };

    var find = function (win, selection, selector) {
      // Reverse the selection if it is RTL when doing the comparison
      var outerRange = SelectionDirection.asLtrRange(win, selection);
      var ancestor = Element.fromDom(outerRange.commonAncestorContainer);
      // Note, this might need to change when we have to start looking for non elements.
      return Node.isElement(ancestor) ? 
        withinContainer(win, ancestor, outerRange, selector) : [];
    };

    return {
      find: find
    };
  }
);
define(
  'ephox.sugar.selection.quirks.Prefilter',

  [
    'ephox.katamari.api.Arr',
    'ephox.sugar.api.node.Node',
    'ephox.sugar.api.selection.Selection',
    'ephox.sugar.api.selection.Situ'
  ],

  function (Arr, Node, Selection, Situ) {
    var beforeSpecial = function (element, offset) {
      // From memory, we don't want to use <br> directly on Firefox because it locks the keyboard input.
      // It turns out that <img> directly on IE locks the keyboard as well.
      // If the offset is 0, use before. If the offset is 1, use after.
      // TBIO-3889: Firefox Situ.on <input> results in a child of the <input>; Situ.before <input> results in platform inconsistencies
      var name = Node.name(element);
      if ('input' === name) return Situ.after(element);
      else if (!Arr.contains([ 'br', 'img' ], name)) return Situ.on(element, offset);
      else return offset === 0 ? Situ.before(element) : Situ.after(element);
    };

    var preprocess = function (startSitu, finishSitu) {
      var start = startSitu.fold(Situ.before, beforeSpecial, Situ.after);
      var finish = finishSitu.fold(Situ.before, beforeSpecial, Situ.after);
      return Selection.relative(start, finish);
    };

    return {
      beforeSpecial: beforeSpecial,
      preprocess: preprocess
    };
  }
);

define(
  'ephox.sugar.api.selection.WindowSelection',

  [
    'ephox.katamari.api.Option',
    'ephox.sugar.api.dom.DocumentPosition',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.node.Fragment',
    'ephox.sugar.api.selection.Selection',
    'ephox.sugar.api.selection.Situ',
    'ephox.sugar.selection.core.NativeRange',
    'ephox.sugar.selection.core.SelectionDirection',
    'ephox.sugar.selection.query.CaretRange',
    'ephox.sugar.selection.query.Within',
    'ephox.sugar.selection.quirks.Prefilter'
  ],

  function (Option, DocumentPosition, Element, Fragment, Selection, Situ, NativeRange, SelectionDirection, CaretRange, Within, Prefilter) {
    var doSetNativeRange = function (win, rng) {
      var selection = win.getSelection();
      selection.removeAllRanges();
      selection.addRange(rng);
    };

    var doSetRange = function (win, start, soffset, finish, foffset) {
      var rng = NativeRange.exactToNative(win, start, soffset, finish, foffset);
      doSetNativeRange(win, rng);
    };

    var findWithin = function (win, selection, selector) {
      return Within.find(win, selection, selector);
    };

    var setExact = function (win, start, soffset, finish, foffset) {
      setRelative(win, Situ.on(start, soffset), Situ.on(finish, foffset));
    };

    var setRelative = function (win, startSitu, finishSitu) {
      var relative = Prefilter.preprocess(startSitu, finishSitu);

      return SelectionDirection.diagnose(win, relative).match({
        ltr: function (start, soffset, finish, foffset) {
          doSetRange(win, start, soffset, finish, foffset);
        },
        rtl: function (start, soffset, finish, foffset) {
          var selection = win.getSelection();
          // If this selection is backwards, then we need to use extend.
          if (selection.extend) {
            selection.collapse(start.dom(), soffset);
            selection.extend(finish.dom(), foffset);
          } else {
            doSetRange(win, finish, foffset, start, soffset);
          }
        }
      });
    };

    // NOTE: We are still reading the range because it gives subtly different behaviour
    // than using the anchorNode and focusNode. I'm not sure if this behaviour is any
    // better or worse; it's just different.
    var readRange = function (selection) {
      var rng = Option.from(selection.getRangeAt(0));
      return rng.map(function (r) {
        return Selection.range(Element.fromDom(r.startContainer), r.startOffset, Element.fromDom(r.endContainer), r.endOffset);
      });
    };

    var doGetExact = function (selection) {
      var anchorNode = Element.fromDom(selection.anchorNode);
      var focusNode = Element.fromDom(selection.focusNode);
      return DocumentPosition.after(anchorNode, selection.anchorOffset, focusNode, selection.focusOffset) ? Option.some(
        Selection.range(
          Element.fromDom(selection.anchorNode),
          selection.anchorOffset,
          Element.fromDom(selection.focusNode),
          selection.focusOffset
        )
      ) : readRange(selection);
    };

    var setToElement = function (win, element) {
      var rng = NativeRange.selectNodeContents(win, element);
      doSetNativeRange(win, rng);
    };

    var forElement = function (win, element) {
      var rng = NativeRange.selectNodeContents(win, element);
      return Selection.range(
        Element.fromDom(rng.startContainer), rng.startOffset, 
        Element.fromDom(rng.endContainer), rng.endOffset
      );
    };

    var getExact = function (win) {
      // We want to retrieve the selection as it is.
      var selection = win.getSelection();
      return selection.rangeCount > 0 ? doGetExact(selection) : Option.none();
    };

    // TODO: Test this.
    var get = function (win) {
      return getExact(win).map(function (range) {
        return Selection.exact(range.start(), range.soffset(), range.finish(), range.foffset());
      });
    };

    var getFirstRect = function (win, selection) {
      var rng = SelectionDirection.asLtrRange(win, selection);
      return NativeRange.getFirstRect(rng);
    };

    var getBounds = function (win, selection) {
      var rng = SelectionDirection.asLtrRange(win, selection);
      return NativeRange.getBounds(rng);
    };

    var getAtPoint = function (win, x, y) {
      return CaretRange.fromPoint(win, x, y);
    };

    var getAsString = function (win, selection) {
      var rng = SelectionDirection.asLtrRange(win, selection);
      return NativeRange.toString(rng);
    };

    var clear = function (win) {
      var selection = win.getSelection();
      selection.removeAllRanges();
    };

    var clone = function (win, selection) {
      var rng = SelectionDirection.asLtrRange(win, selection);
      return NativeRange.cloneFragment(rng);
    };

    var replace = function (win, selection, elements) {
      var rng = SelectionDirection.asLtrRange(win, selection);
      var fragment = Fragment.fromElements(elements);
      NativeRange.replaceWith(rng, fragment);
    };

    var deleteAt = function (win, selection) {
      var rng = SelectionDirection.asLtrRange(win, selection);
      NativeRange.deleteContents(rng);
    };

    return {
      setExact: setExact,
      getExact: getExact,
      get: get,
      setRelative: setRelative,
      setToElement: setToElement,
      clear: clear,

      clone: clone,
      replace: replace,
      deleteAt: deleteAt,

      forElement: forElement,

      getFirstRect: getFirstRect,
      getBounds: getBounds,
      getAtPoint: getAtPoint,

      findWithin: findWithin,
      getAsString: getAsString
    };
  }
);

define(
  'tinymce.core.selection.SelectionBookmark',

  [
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.node.Node',
    'ephox.sugar.api.node.Text',
    'ephox.sugar.api.search.Traverse',
    'ephox.sugar.api.selection.Selection',
    'ephox.sugar.api.selection.WindowSelection',
    'global!document',
    'tinymce.core.caret.CaretFinder'
  ],

  function (Fun, Option, Compare, Element, Node, Text, Traverse, Selection, WindowSelection, document, CaretFinder) {
    var clamp = function (offset, element) {
      var max = Node.isText(element) ? Text.get(element).length : Traverse.children(element).length + 1;

      if (offset > max) {
        return max;
      } else if (offset < 0) {
        return 0;
      }

      return offset;
    };

    var normalizeRng = function (rng) {
      return Selection.range(
        rng.start(),
        clamp(rng.soffset(), rng.start()),
        rng.finish(),
        clamp(rng.foffset(), rng.finish())
      );
    };

    var isOrContains = function (root, elm) {
      return Compare.contains(root, elm) || Compare.eq(root, elm);
    };

    var isRngInRoot = function (root) {
      return function (rng) {
        return isOrContains(root, rng.start()) && isOrContains(root, rng.finish());
      };
    };

    // var dumpRng = function (rng) {
    //   console.log('start', rng.start().dom());
    //   console.log('soffset', rng.soffset());
    //   console.log('finish', rng.finish().dom());
    //   console.log('foffset', rng.foffset());
    //   return rng;
    // };

    var readRange = function (win) {
      var selection = win.getSelection();
      var rng = selection.rangeCount === 0 ? Option.none() : Option.from(selection.getRangeAt(0));
      return rng.map(function (r) {
        return Selection.range(Element.fromDom(r.startContainer), r.startOffset, Element.fromDom(r.endContainer), r.endOffset);
      });
    };

    var getBookmark = function (root) {
      var win = Traverse.defaultView(root);

      return readRange(win.dom())
        .filter(isRngInRoot(root));
    };

    var validate = function (root, bookmark) {
      return Option.from(bookmark)
          .filter(isRngInRoot(root))
          .map(normalizeRng);
    };

    var bookmarkToNativeRng = function (bookmark) {
      var rng = document.createRange();
      rng.setStart(bookmark.start().dom(), bookmark.soffset());
      rng.setEnd(bookmark.finish().dom(), bookmark.foffset());

      return Option.some(rng);
    };

    var store = function (editor) {
      var newBookmark = getBookmark(Element.fromDom(editor.getBody()));

      editor.bookmark = newBookmark.isSome() ? newBookmark : editor.bookmark;
    };

    var getRng = function (editor) {
      var bookmark = editor.bookmark ? editor.bookmark : Option.none();

      return bookmark
        .bind(Fun.curry(validate, Element.fromDom(editor.getBody())))
        .bind(bookmarkToNativeRng);
    };

    var restore = function (editor) {
      getRng(editor).each(function (rng) {
        editor.selection.setRng(rng);
      });
    };

    return {
      store: store,
      restore: restore,
      getRng: getRng,
      getBookmark: getBookmark,
      validate: validate
    };
  }
);

/**
 * WindowManagerImpl.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
 */

define(
  'tinymce.core.ui.WindowManagerImpl',
  [
  ],
  function () {
    return function () {
      var unimplemented = function () {
        throw new Error('Theme did not provide a WindowManager implementation.');
      };

      return {
        open: unimplemented,
        alert: unimplemented,
        confirm: unimplemented,
        close: unimplemented,
        getParams: unimplemented,
        setParams: unimplemented
      };
    };
  }
);

/**
 * WindowManager.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
 */

/**
 * This class handles the creation of native windows and dialogs. This class can be extended to provide for example inline dialogs.
 *
 * @class tinymce.WindowManager
 * @example
 * // Opens a new dialog with the file.htm file and the size 320x240
 * // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog.
 * tinymce.activeEditor.windowManager.open({
 *    url: 'file.htm',
 *    width: 320,
 *    height: 240
 * }, {
 *    custom_param: 1
 * });
 *
 * // Displays an alert box using the active editors window manager instance
 * tinymce.activeEditor.windowManager.alert('Hello world!');
 *
 * // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm
 * });
 */
define(
  'tinymce.core.api.WindowManager',
  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Option',
    'tinymce.core.selection.SelectionBookmark',
    'tinymce.core.ui.WindowManagerImpl'
  ],
  function (Arr, Option, SelectionBookmark, WindowManagerImpl) {
    return function (editor) {
      var windows = [];

      var getImplementation = function () {
        var theme = editor.theme;
        return theme && theme.getWindowManagerImpl ? theme.getWindowManagerImpl() : WindowManagerImpl();
      };

      var funcBind = function (scope, f) {
        return function () {
          return f ? f.apply(scope, arguments) : undefined;
        };
      };

      var fireOpenEvent = function (win) {
        editor.fire('OpenWindow', {
          win: win
        });
      };

      var fireCloseEvent = function (win) {
        editor.fire('CloseWindow', {
          win: win
        });
      };

      var addWindow = function (win) {
        windows.push(win);
        fireOpenEvent(win);
      };

      var closeWindow = function (win) {
        Arr.findIndex(windows, function (otherWindow) {
          return otherWindow === win;
        }).each(function (index) {
          // Mutate here since third party might have stored away the window array, consider breaking this api
          windows.splice(index, 1);

          fireCloseEvent(win);

          // Move focus back to editor when the last window is closed
          if (windows.length === 0) {
            editor.focus();
          }
        });
      };

      var getTopWindow = function () {
        return Option.from(windows[windows.length - 1]);
      };

      var open = function (args, params) {
        editor.editorManager.setActive(editor);
        SelectionBookmark.store(editor);

        var win = getImplementation().open(args, params, closeWindow);
        addWindow(win);
        return win;
      };

      var alert = function (message, callback, scope) {
        var win = getImplementation().alert(message, funcBind(scope ? scope : this, callback), closeWindow);
        addWindow(win);
      };

      var confirm = function (message, callback, scope) {
        var win = getImplementation().confirm(message, funcBind(scope ? scope : this, callback), closeWindow);
        addWindow(win);
      };

      var close = function () {
        getTopWindow().each(function (win) {
          getImplementation().close(win);
          closeWindow(win);
        });
      };

      var getParams = function () {
        return getTopWindow().map(getImplementation().getParams).getOr(null);
      };

      var setParams = function (params) {
        getTopWindow().each(function (win) {
          getImplementation().setParams(win, params);
        });
      };

      var getWindows = function () {
        return windows;
      };

      editor.on('remove', function () {
        Arr.each(windows.slice(0), function (win) {
          getImplementation().close(win);
        });
      });

      return {
        // Used by the legacy3x compat layer and possible third party
        // TODO: Deprecate this, and possible switch to a immutable window array for getWindows
        windows: windows,

        /**
         * Opens a new window.
         *
         * @method open
         * @param {Object} args Optional name/value settings collection contains things like width/height/url etc.
         * @param {Object} params Options like title, file, width, height etc.
         * @option {String} title Window title.
         * @option {String} file URL of the file to open in the window.
         * @option {Number} width Width in pixels.
         * @option {Number} height Height in pixels.
         * @option {Boolean} autoScroll Specifies whether the popup window can have scrollbars if required (i.e. content
         * larger than the popup size specified).
         */
        open: open,

        /**
         * Creates a alert dialog. Please don't use the blocking behavior of this
         * native version use the callback method instead then it can be extended.
         *
         * @method alert
         * @param {String} message Text to display in the new alert dialog.
         * @param {function} callback Callback function to be executed after the user has selected ok.
         * @param {Object} scope Optional scope to execute the callback in.
         * @example
         * // Displays an alert box using the active editors window manager instance
         * tinymce.activeEditor.windowManager.alert('Hello world!');
         */
        alert: alert,

        /**
         * Creates a confirm dialog. Please don't use the blocking behavior of this
         * native version use the callback method instead then it can be extended.
         *
         * @method confirm
         * @param {String} message Text to display in the new confirm dialog.
         * @param {function} callback Callback function to be executed after the user has selected ok or cancel.
         * @param {Object} scope Optional scope to execute the callback in.
         * @example
         * // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm
         * tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s) {
         *    if (s)
         *       tinymce.activeEditor.windowManager.alert("Ok");
         *    else
         *       tinymce.activeEditor.windowManager.alert("Cancel");
         * });
         */
        confirm: confirm,

        /**
         * Closes the top most window.
         *
         * @method close
         */
        close: close,

        /**
         * Returns the params of the last window open call. This can be used in iframe based
         * dialog to get params passed from the tinymce plugin.
         *
         * @example
         * var dialogArguments = top.tinymce.activeEditor.windowManager.getParams();
         *
         * @method getParams
         * @return {Object} Name/value object with parameters passed from windowManager.open call.
         */
        getParams: getParams,

        /**
         * Sets the params of the last opened window.
         *
         * @method setParams
         * @param {Object} params Params object to set for the last opened window.
         */
        setParams: setParams,

        /**
         * Returns the currently opened window objects.
         *
         * @method getWindows
         * @return {Array} Array of the currently opened windows.
         */
        getWindows: getWindows
      };
    };
  }
);

/**
 * RangePoint.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
 */

define(
  'tinymce.core.dom.RangePoint',
  [
    'ephox.katamari.api.Arr',
    'tinymce.core.geom.ClientRect'
  ],
  function (Arr, ClientRect) {
    var isXYWithinRange = function (clientX, clientY, range) {
      if (range.collapsed) {
        return false;
      }

      return Arr.foldl(range.getClientRects(), function (state, rect) {
        return state || ClientRect.containsXY(rect, clientX, clientY);
      }, false);
    };

    return {
      isXYWithinRange: isXYWithinRange
    };
  }
);
/**
 * VK.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
 */

/**
 * This file exposes a set of the common KeyCodes for use. Please grow it as needed.
 */
define(
  'tinymce.core.util.VK',
  [
    "tinymce.core.Env"
  ],
  function (Env) {
    return {
      BACKSPACE: 8,
      DELETE: 46,
      DOWN: 40,
      ENTER: 13,
      LEFT: 37,
      RIGHT: 39,
      SPACEBAR: 32,
      TAB: 9,
      UP: 38,

      modifierPressed: function (e) {
        return e.shiftKey || e.ctrlKey || e.altKey || this.metaKeyPressed(e);
      },

      metaKeyPressed: function (e) {
        // Check if ctrl or meta key is pressed. Edge case for AltGr on Windows where it produces ctrlKey+altKey states
        return (Env.mac ? e.metaKey : e.ctrlKey && !e.altKey);
      }
    };
  }
);

/**
 * ControlSelection.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
 */

/**
 * This class handles control selection of elements. Controls are elements
 * that can be resized and needs to be selected as a whole. It adds custom resize handles
 * to all browser engines that support properly disabling the built in resize logic.
 *
 * @class tinymce.dom.ControlSelection
 */
define(
  'tinymce.core.dom.ControlSelection',
  [
    'ephox.katamari.api.Fun',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.search.Selectors',
    'global!document',
    'tinymce.core.dom.NodeType',
    'tinymce.core.dom.RangePoint',
    'tinymce.core.Env',
    'tinymce.core.util.Delay',
    'tinymce.core.util.Tools',
    'tinymce.core.util.VK'
  ],
  function (Fun, Element, Selectors, document, NodeType, RangePoint, Env, Delay, Tools, VK) {
    var isContentEditableFalse = NodeType.isContentEditableFalse;
    var isContentEditableTrue = NodeType.isContentEditableTrue;

    var getContentEditableRoot = function (root, node) {
      while (node && node != root) {
        if (isContentEditableTrue(node) || isContentEditableFalse(node)) {
          return node;
        }

        node = node.parentNode;
      }

      return null;
    };

    var isImage = function (elm) {
      return elm && elm.nodeName === 'IMG';
    };

    var isEventOnImageOutsideRange = function (evt, range) {
      return isImage(evt.target) && !RangePoint.isXYWithinRange(evt.clientX, evt.clientY, range);
    };

    var contextMenuSelectImage = function (editor, evt) {
      var target = evt.target;

      if (isEventOnImageOutsideRange(evt, editor.selection.getRng()) && !evt.isDefaultPrevented()) {
        evt.preventDefault();
        editor.selection.select(target);
      }
    };

    return function (selection, editor) {
      var dom = editor.dom, each = Tools.each;
      var selectedElm, selectedElmGhost, resizeHelper, resizeHandles, selectedHandle, lastMouseDownEvent;
      var startX, startY, selectedElmX, selectedElmY, startW, startH, ratio, resizeStarted;
      var width, height, editableDoc = editor.getDoc(), rootDocument = document, isIE = Env.ie && Env.ie < 11;
      var abs = Math.abs, round = Math.round, rootElement = editor.getBody(), startScrollWidth, startScrollHeight;

      // Details about each resize handle how to scale etc
      resizeHandles = {
        // Name: x multiplier, y multiplier, delta size x, delta size y
        /*n: [0.5, 0, 0, -1],
        e: [1, 0.5, 1, 0],
        s: [0.5, 1, 0, 1],
        w: [0, 0.5, -1, 0],*/
        nw: [0, 0, -1, -1],
        ne: [1, 0, 1, -1],
        se: [1, 1, 1, 1],
        sw: [0, 1, -1, 1]
      };

      // Add CSS for resize handles, cloned element and selected
      var rootClass = '.mce-content-body';
      editor.contentStyles.push(
        rootClass + ' div.mce-resizehandle {' +
        'position: absolute;' +
        'border: 1px solid black;' +
        'box-sizing: box-sizing;' +
        'background: #FFF;' +
        'width: 7px;' +
        'height: 7px;' +
        'z-index: 10000' +
        '}' +
        rootClass + ' .mce-resizehandle:hover {' +
        'background: #000' +
        '}' +
        rootClass + ' img[data-mce-selected],' + rootClass + ' hr[data-mce-selected] {' +
        'outline: 1px solid black;' +
        'resize: none' + // Have been talks about implementing this in browsers
        '}' +
        rootClass + ' .mce-clonedresizable {' +
        'position: absolute;' +
        (Env.gecko ? '' : 'outline: 1px dashed black;') + // Gecko produces trails while resizing
        'opacity: .5;' +
        'filter: alpha(opacity=50);' +
        'z-index: 10000' +
        '}' +
        rootClass + ' .mce-resize-helper {' +
        'background: #555;' +
        'background: rgba(0,0,0,0.75);' +
        'border-radius: 3px;' +
        'border: 1px;' +
        'color: white;' +
        'display: none;' +
        'font-family: sans-serif;' +
        'font-size: 12px;' +
        'white-space: nowrap;' +
        'line-height: 14px;' +
        'margin: 5px 10px;' +
        'padding: 5px;' +
        'position: absolute;' +
        'z-index: 10001' +
        '}'
      );

      var isResizable = function (elm) {
        var selector = editor.settings.object_resizing;

        if (selector === false || Env.iOS) {
          return false;
        }

        if (typeof selector != 'string') {
          selector = 'table,img,div';
        }

        if (elm.getAttribute('data-mce-resize') === 'false') {
          return false;
        }

        if (elm == editor.getBody()) {
          return false;
        }

        return Selectors.is(Element.fromDom(elm), selector);
      };

      var resizeGhostElement = function (e) {
        var deltaX, deltaY, proportional;
        var resizeHelperX, resizeHelperY;

        // Calc new width/height
        deltaX = e.screenX - startX;
        deltaY = e.screenY - startY;

        // Calc new size
        width = deltaX * selectedHandle[2] + startW;
        height = deltaY * selectedHandle[3] + startH;

        // Never scale down lower than 5 pixels
        width = width < 5 ? 5 : width;
        height = height < 5 ? 5 : height;

        if (selectedElm.nodeName == "IMG" && editor.settings.resize_img_proportional !== false) {
          proportional = !VK.modifierPressed(e);
        } else {
          proportional = VK.modifierPressed(e) || (selectedElm.nodeName == "IMG" && selectedHandle[2] * selectedHandle[3] !== 0);
        }

        // Constrain proportions
        if (proportional) {
          if (abs(deltaX) > abs(deltaY)) {
            height = round(width * ratio);
            width = round(height / ratio);
          } else {
            width = round(height / ratio);
            height = round(width * ratio);
          }
        }

        // Update ghost size
        dom.setStyles(selectedElmGhost, {
          width: width,
          height: height
        });

        // Update resize helper position
        resizeHelperX = selectedHandle.startPos.x + deltaX;
        resizeHelperY = selectedHandle.startPos.y + deltaY;
        resizeHelperX = resizeHelperX > 0 ? resizeHelperX : 0;
        resizeHelperY = resizeHelperY > 0 ? resizeHelperY : 0;

        dom.setStyles(resizeHelper, {
          left: resizeHelperX,
          top: resizeHelperY,
          display: 'block'
        });

        resizeHelper.innerHTML = width + ' &times; ' + height;

        // Update ghost X position if needed
        if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) {
          dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width));
        }

        // Update ghost Y position if needed
        if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) {
          dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height));
        }

        // Calculate how must overflow we got
        deltaX = rootElement.scrollWidth - startScrollWidth;
        deltaY = rootElement.scrollHeight - startScrollHeight;

        // Re-position the resize helper based on the overflow
        if (deltaX + deltaY !== 0) {
          dom.setStyles(resizeHelper, {
            left: resizeHelperX - deltaX,
            top: resizeHelperY - deltaY
          });
        }

        if (!resizeStarted) {
          editor.fire('ObjectResizeStart', { target: selectedElm, width: startW, height: startH });
          resizeStarted = true;
        }
      };

      var endGhostResize = function () {
        resizeStarted = false;

        var setSizeProp = function (name, value) {
          if (value) {
            // Resize by using style or attribute
            if (selectedElm.style[name] || !editor.schema.isValid(selectedElm.nodeName.toLowerCase(), name)) {
              dom.setStyle(selectedElm, name, value);
            } else {
              dom.setAttrib(selectedElm, name, value);
            }
          }
        };

        // Set width/height properties
        setSizeProp('width', width);
        setSizeProp('height', height);

        dom.unbind(editableDoc, 'mousemove', resizeGhostElement);
        dom.unbind(editableDoc, 'mouseup', endGhostResize);

        if (rootDocument != editableDoc) {
          dom.unbind(rootDocument, 'mousemove', resizeGhostElement);
          dom.unbind(rootDocument, 'mouseup', endGhostResize);
        }

        // Remove ghost/helper and update resize handle positions
        dom.remove(selectedElmGhost);
        dom.remove(resizeHelper);

        if (!isIE || selectedElm.nodeName == "TABLE") {
          showResizeRect(selectedElm);
        }

        editor.fire('ObjectResized', { target: selectedElm, width: width, height: height });
        dom.setAttrib(selectedElm, 'style', dom.getAttrib(selectedElm, 'style'));
        editor.nodeChanged();
      };

      var showResizeRect = function (targetElm, mouseDownHandleName, mouseDownEvent) {
        var position, targetWidth, targetHeight, e, rect;

        hideResizeRect();
        unbindResizeHandleEvents();

        // Get position and size of target
        position = dom.getPos(targetElm, rootElement);
        selectedElmX = position.x;
        selectedElmY = position.y;
        rect = targetElm.getBoundingClientRect(); // Fix for Gecko offsetHeight for table with caption
        targetWidth = rect.width || (rect.right - rect.left);
        targetHeight = rect.height || (rect.bottom - rect.top);

        // Reset width/height if user selects a new image/table
        if (selectedElm != targetElm) {
          detachResizeStartListener();
          selectedElm = targetElm;
          width = height = 0;
        }

        // Makes it possible to disable resizing
        e = editor.fire('ObjectSelected', { target: targetElm });

        if (isResizable(targetElm) && !e.isDefaultPrevented()) {
          each(resizeHandles, function (handle, name) {
            var handleElm;

            var startDrag = function (e) {
              startX = e.screenX;
              startY = e.screenY;
              startW = selectedElm.clientWidth;
              startH = selectedElm.clientHeight;
              ratio = startH / startW;
              selectedHandle = handle;

              handle.startPos = {
                x: targetWidth * handle[0] + selectedElmX,
                y: targetHeight * handle[1] + selectedElmY
              };

              startScrollWidth = rootElement.scrollWidth;
              startScrollHeight = rootElement.scrollHeight;

              selectedElmGhost = selectedElm.cloneNode(true);
              dom.addClass(selectedElmGhost, 'mce-clonedresizable');
              dom.setAttrib(selectedElmGhost, 'data-mce-bogus', 'all');
              selectedElmGhost.contentEditable = false; // Hides IE move layer cursor
              selectedElmGhost.unSelectabe = true;
              dom.setStyles(selectedElmGhost, {
                left: selectedElmX,
                top: selectedElmY,
                margin: 0
              });

              selectedElmGhost.removeAttribute('data-mce-selected');
              rootElement.appendChild(selectedElmGhost);

              dom.bind(editableDoc, 'mousemove', resizeGhostElement);
              dom.bind(editableDoc, 'mouseup', endGhostResize);

              if (rootDocument != editableDoc) {
                dom.bind(rootDocument, 'mousemove', resizeGhostElement);
                dom.bind(rootDocument, 'mouseup', endGhostResize);
              }

              resizeHelper = dom.add(rootElement, 'div', {
                'class': 'mce-resize-helper',
                'data-mce-bogus': 'all'
              }, startW + ' &times; ' + startH);
            };

            if (mouseDownHandleName) {
              // Drag started by IE native resizestart
              if (name == mouseDownHandleName) {
                startDrag(mouseDownEvent);
              }

              return;
            }

            // Get existing or render resize handle
            handleElm = dom.get('mceResizeHandle' + name);
            if (handleElm) {
              dom.remove(handleElm);
            }

            handleElm = dom.add(rootElement, 'div', {
              id: 'mceResizeHandle' + name,
              'data-mce-bogus': 'all',
              'class': 'mce-resizehandle',
              unselectable: true,
              style: 'cursor:' + name + '-resize; margin:0; padding:0'
            });

            // Hides IE move layer cursor
            // If we set it on Chrome we get this wounderful bug: #6725
            if (Env.ie) {
              handleElm.contentEditable = false;
            }

            dom.bind(handleElm, 'mousedown', function (e) {
              e.stopImmediatePropagation();
              e.preventDefault();
              startDrag(e);
            });

            handle.elm = handleElm;

            // Position element
            dom.setStyles(handleElm, {
              left: (targetWidth * handle[0] + selectedElmX) - (handleElm.offsetWidth / 2),
              top: (targetHeight * handle[1] + selectedElmY) - (handleElm.offsetHeight / 2)
            });
          });
        } else {
          hideResizeRect();
        }

        selectedElm.setAttribute('data-mce-selected', '1');
      };

      var hideResizeRect = function () {
        var name, handleElm;

        unbindResizeHandleEvents();

        if (selectedElm) {
          selectedElm.removeAttribute('data-mce-selected');
        }

        for (name in resizeHandles) {
          handleElm = dom.get('mceResizeHandle' + name);
          if (handleElm) {
            dom.unbind(handleElm);
            dom.remove(handleElm);
          }
        }
      };

      var updateResizeRect = function (e) {
        var startElm, controlElm;

        var isChildOrEqual = function (node, parent) {
          if (node) {
            do {
              if (node === parent) {
                return true;
              }
            } while ((node = node.parentNode));
          }
        };

        // Ignore all events while resizing or if the editor instance was removed
        if (resizeStarted || editor.removed) {
          return;
        }

        // Remove data-mce-selected from all elements since they might have been copied using Ctrl+c/v
        each(dom.select('img[data-mce-selected],hr[data-mce-selected]'), function (img) {
          img.removeAttribute('data-mce-selected');
        });

        controlElm = e.type == 'mousedown' ? e.target : selection.getNode();
        controlElm = dom.$(controlElm).closest(isIE ? 'table' : 'table,img,hr')[0];

        if (isChildOrEqual(controlElm, rootElement)) {
          disableGeckoResize();
          startElm = selection.getStart(true);

          if (isChildOrEqual(startElm, controlElm) && isChildOrEqual(selection.getEnd(true), controlElm)) {
            if (!isIE || (controlElm != startElm && startElm.nodeName !== 'IMG')) {
              showResizeRect(controlElm);
              return;
            }
          }
        }

        hideResizeRect();
      };

      var attachEvent = function (elm, name, func) {
        if (elm && elm.attachEvent) {
          elm.attachEvent('on' + name, func);
        }
      };

      var detachEvent = function (elm, name, func) {
        if (elm && elm.detachEvent) {
          elm.detachEvent('on' + name, func);
        }
      };

      var resizeNativeStart = function (e) {
        var target = e.srcElement, pos, name, corner, cornerX, cornerY, relativeX, relativeY;

        pos = target.getBoundingClientRect();
        relativeX = lastMouseDownEvent.clientX - pos.left;
        relativeY = lastMouseDownEvent.clientY - pos.top;

        // Figure out what corner we are draging on
        for (name in resizeHandles) {
          corner = resizeHandles[name];

          cornerX = target.offsetWidth * corner[0];
          cornerY = target.offsetHeight * corner[1];

          if (abs(cornerX - relativeX) < 8 && abs(cornerY - relativeY) < 8) {
            selectedHandle = corner;
            break;
          }
        }

        // Remove native selection and let the magic begin
        resizeStarted = true;
        editor.fire('ObjectResizeStart', {
          target: selectedElm,
          width: selectedElm.clientWidth,
          height: selectedElm.clientHeight
        });
        editor.getDoc().selection.empty();
        showResizeRect(target, name, lastMouseDownEvent);
      };

      var preventDefault = function (e) {
        if (e.preventDefault) {
          e.preventDefault();
        } else {
          e.returnValue = false; // IE
        }
      };

      var isWithinContentEditableFalse = function (elm) {
        return isContentEditableFalse(getContentEditableRoot(editor.getBody(), elm));
      };

      var nativeControlSelect = function (e) {
        var target = e.srcElement;

        if (isWithinContentEditableFalse(target)) {
          preventDefault(e);
          return;
        }

        if (target != selectedElm) {
          editor.fire('ObjectSelected', { target: target });
          detachResizeStartListener();

          if (target.id.indexOf('mceResizeHandle') === 0) {
            e.returnValue = false;
            return;
          }

          if (target.nodeName == 'IMG' || target.nodeName == 'TABLE') {
            hideResizeRect();
            selectedElm = target;
            attachEvent(target, 'resizestart', resizeNativeStart);
          }
        }
      };

      var detachResizeStartListener = function () {
        detachEvent(selectedElm, 'resizestart', resizeNativeStart);
      };

      var unbindResizeHandleEvents = function () {
        for (var name in resizeHandles) {
          var handle = resizeHandles[name];

          if (handle.elm) {
            dom.unbind(handle.elm);
            delete handle.elm;
          }
        }
      };

      var disableGeckoResize = function () {
        try {
          // Disable object resizing on Gecko
          editor.getDoc().execCommand('enableObjectResizing', false, false);
        } catch (ex) {
          // Ignore
        }
      };

      var controlSelect = function (elm) {
        var ctrlRng;

        if (!isIE) {
          return;
        }

        ctrlRng = editableDoc.body.createControlRange();

        try {
          ctrlRng.addElement(elm);
          ctrlRng.select();
          return true;
        } catch (ex) {
          // Ignore since the element can't be control selected for example a P tag
        }
      };

      editor.on('init', function () {
        if (isIE) {
          // Hide the resize rect on resize and reselect the image
          editor.on('ObjectResized', function (e) {
            if (e.target.nodeName != 'TABLE') {
              hideResizeRect();
              controlSelect(e.target);
            }
          });

          attachEvent(rootElement, 'controlselect', nativeControlSelect);

          editor.on('mousedown', function (e) {
            lastMouseDownEvent = e;
          });
        } else {
          disableGeckoResize();

          // Sniff sniff, hard to feature detect this stuff
          if (Env.ie >= 11) {
            // Needs to be mousedown for drag/drop to work on IE 11
            // Needs to be click on Edge to properly select images
            editor.on('mousedown click', function (e) {
              var target = e.target, nodeName = target.nodeName;

              if (!resizeStarted && /^(TABLE|IMG|HR)$/.test(nodeName) && !isWithinContentEditableFalse(target)) {
                if (e.button !== 2) {
                  editor.selection.select(target, nodeName == 'TABLE');
                }

                // Only fire once since nodeChange is expensive
                if (e.type == 'mousedown') {
                  editor.nodeChanged();
                }
              }
            });

            editor.dom.bind(rootElement, 'mscontrolselect', function (e) {
              var delayedSelect = function (node) {
                Delay.setEditorTimeout(editor, function () {
                  editor.selection.select(node);
                });
              };

              if (isWithinContentEditableFalse(e.target)) {
                e.preventDefault();
                delayedSelect(e.target);
                return;
              }

              if (/^(TABLE|IMG|HR)$/.test(e.target.nodeName)) {
                e.preventDefault();

                // This moves the selection from being a control selection to a text like selection like in WebKit #6753
                // TODO: Fix this the day IE works like other browsers without this nasty native ugly control selections.
                if (e.target.tagName == 'IMG') {
                  delayedSelect(e.target);
                }
              }
            });
          }
        }

        var throttledUpdateResizeRect = Delay.throttle(function (e) {
          if (!editor.composing) {
            updateResizeRect(e);
          }
        });

        editor.on('nodechange ResizeEditor ResizeWindow drop', throttledUpdateResizeRect);

        // Update resize rect while typing in a table
        editor.on('keyup compositionend', function (e) {
          // Don't update the resize rect while composing since it blows away the IME see: #2710
          if (selectedElm && selectedElm.nodeName == "TABLE") {
            throttledUpdateResizeRect(e);
          }
        });

        editor.on('hide blur', hideResizeRect);
        editor.on('contextmenu', Fun.curry(contextMenuSelectImage, editor));

        // Hide rect on focusout since it would float on top of windows otherwise
        //editor.on('focusout', hideResizeRect);
      });

      editor.on('remove', unbindResizeHandleEvents);

      var destroy = function () {
        selectedElm = selectedElmGhost = null;

        if (isIE) {
          detachResizeStartListener();
          detachEvent(rootElement, 'controlselect', nativeControlSelect);
        }
      };

      return {
        isResizable: isResizable,
        showResizeRect: showResizeRect,
        hideResizeRect: hideResizeRect,
        updateResizeRect: updateResizeRect,
        controlSelect: controlSelect,
        destroy: destroy
      };
    };
  }
);

define(
  'ephox.sugar.api.search.PredicateExists',

  [
    'ephox.sugar.api.search.PredicateFind'
  ],

  function (PredicateFind) {
    var any = function (predicate) {
      return PredicateFind.first(predicate).isSome();
    };

    var ancestor = function (scope, predicate, isRoot) {
      return PredicateFind.ancestor(scope, predicate, isRoot).isSome();
    };

    var closest = function (scope, predicate, isRoot) {
      return PredicateFind.closest(scope, predicate, isRoot).isSome();
    };

    var sibling = function (scope, predicate) {
      return PredicateFind.sibling(scope, predicate).isSome();
    };

    var child = function (scope, predicate) {
      return PredicateFind.child(scope, predicate).isSome();
    };

    var descendant = function (scope, predicate) {
      return PredicateFind.descendant(scope, predicate).isSome();
    };

    return {
      any: any,
      ancestor: ancestor,
      closest: closest,
      sibling: sibling,
      child: child,
      descendant: descendant
    };
  }
);

define(
  'ephox.sugar.api.dom.Focus',

  [
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.search.PredicateExists',
    'ephox.sugar.api.search.Traverse',
    'global!document'
  ],

  function (Fun, Option, Compare, Element, PredicateExists, Traverse, document) {
    var focus = function (element) {
      element.dom().focus();
    };

    var blur = function (element) {
      element.dom().blur();
    };

    var hasFocus = function (element) {
      var doc = Traverse.owner(element).dom();
      return element.dom() === doc.activeElement;
    };

    var active = function (_doc) {
      var doc = _doc !== undefined ? _doc.dom() : document;
      return Option.from(doc.activeElement).map(Element.fromDom);
    };

    var focusInside = function (element) {
      // Only call focus if the focus is not already inside it.
      var doc = Traverse.owner(element);
      var inside = active(doc).filter(function (a) {
        return PredicateExists.closest(a, Fun.curry(Compare.eq, element));
      });

      inside.fold(function () {
        focus(element);
      }, Fun.noop);
    };

    /**
     * Return the descendant element that has focus.
     * Use instead of SelectorFind.descendant(container, ':focus')
     *  because the :focus selector relies on keyboard focus.
     */
    var search = function (element) {
      return active(Traverse.owner(element)).filter(function (e) {
        return element.dom().contains(e.dom());
      });
    };

    return {
      hasFocus: hasFocus,
      focus: focus,
      blur: blur,
      active: active,
      search: search,
      focusInside: focusInside
    };
  }
);
/**
 * EditorFocus.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
 */

define(
  'tinymce.core.EditorFocus',
  [
    'ephox.katamari.api.Option',
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.dom.Focus',
    'ephox.sugar.api.node.Element',
    'tinymce.core.Env',
    'tinymce.core.caret.CaretFinder',
    'tinymce.core.dom.ElementType',
    'tinymce.core.dom.RangeUtils',
    'tinymce.core.selection.SelectionBookmark'
  ],
  function (Option, Compare, Focus, Element, Env, CaretFinder, ElementType, RangeUtils, SelectionBookmark) {
    var getContentEditableHost = function (editor, node) {
      return editor.dom.getParent(node, function (node) {
        return editor.dom.getContentEditable(node) === "true";
      });
    };

    var getCollapsedNode = function (rng) {
      return rng.collapsed ? Option.from(RangeUtils.getNode(rng.startContainer, rng.startOffset)).map(Element.fromDom) : Option.none();
    };

    var getFocusInElement = function (root, rng) {
      return getCollapsedNode(rng).bind(function (node) {
        if (ElementType.isTableSection(node)) {
          return Option.some(node);
        } else if (Compare.contains(root, node) === false) {
          return Option.some(root);
        } else {
          return Option.none();
        }
      });
    };

    var normalizeSelection = function (editor, rng) {
      getFocusInElement(Element.fromDom(editor.getBody()), rng).bind(function (elm) {
        return CaretFinder.firstPositionIn(elm.dom());
      }).fold(
        function () {
          editor.selection.normalize();
        },
        function (caretPos) {
          editor.selection.setRng(caretPos.toRange());
        }
      );
    };

    var focusBody = function (body) {
      if (body.setActive) {
        // IE 11 sometimes throws "Invalid function" then fallback to focus
        // setActive is better since it doesn't scroll to the element being focused
        try {
          body.setActive();
        } catch (ex) {
          body.focus();
        }
      } else {
        body.focus();
      }
    };

    var hasElementFocus = function (elm) {
      return Focus.hasFocus(elm) || Focus.search(elm).isSome();
    };

    var hasIframeFocus = function (editor) {
      return editor.iframeElement && Focus.hasFocus(Element.fromDom(editor.iframeElement));
    };

    var hasInlineFocus = function (editor) {
      var rawBody = editor.getBody();
      return rawBody && hasElementFocus(Element.fromDom(rawBody));
    };

    var hasFocus = function (editor) {
      return editor.inline ? hasInlineFocus(editor) : hasIframeFocus(editor);
    };

    var focusEditor = function (editor) {
      var selection = editor.selection, contentEditable = editor.settings.content_editable;
      var body = editor.getBody(), contentEditableHost, rng = selection.getRng();

      editor.quirks.refreshContentEditable();

      // Move focus to contentEditable=true child if needed
      contentEditableHost = getContentEditableHost(editor, selection.getNode());
      if (editor.$.contains(body, contentEditableHost)) {
        focusBody(contentEditableHost);
        normalizeSelection(editor, rng);
        activateEditor(editor);
        return;
      }

      if (editor.bookmark !== undefined && hasFocus(editor) === false) {
        SelectionBookmark.getRng(editor).each(function (bookmarkRng) {
          editor.selection.setRng(bookmarkRng);
          rng = bookmarkRng;
        });
      }

      // Focus the window iframe
      if (!contentEditable) {
        // WebKit needs this call to fire focusin event properly see #5948
        // But Opera pre Blink engine will produce an empty selection so skip Opera
        if (!Env.opera) {
          focusBody(body);
        }

        editor.getWin().focus();
      }

      // Focus the body as well since it's contentEditable
      if (Env.gecko || contentEditable) {
        focusBody(body);
        normalizeSelection(editor, rng);
      }

      activateEditor(editor);
    };

    var activateEditor = function (editor) {
      editor.editorManager.setActive(editor);
    };

    var focus = function (editor, skipFocus) {
      if (editor.removed) {
        return;
      }

      skipFocus ? activateEditor(editor) : focusEditor(editor);
    };

    return {
      focus: focus,
      hasFocus: hasFocus
    };
  }
);

/**
 * ScrollIntoView.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
 */

define(
  'tinymce.core.dom.ScrollIntoView',
  [
    'tinymce.core.dom.NodeType'
  ],
  function (NodeType) {
    var getPos = function (elm) {
      var x = 0, y = 0;

      var offsetParent = elm;
      while (offsetParent && offsetParent.nodeType) {
        x += offsetParent.offsetLeft || 0;
        y += offsetParent.offsetTop || 0;
        offsetParent = offsetParent.offsetParent;
      }

      return { x: x, y: y };
    };

    var fireScrollIntoViewEvent = function (editor, elm, alignToTop) {
      var scrollEvent = { elm: elm, alignToTop: alignToTop };
      editor.fire('scrollIntoView', scrollEvent);
      return scrollEvent.isDefaultPrevented();
    };

    var scrollIntoView = function (editor, elm, alignToTop) {
      var y, viewPort, dom = editor.dom, root = dom.getRoot(), viewPortY, viewPortH, offsetY = 0;

      if (fireScrollIntoViewEvent(editor, elm, alignToTop)) {
        return;
      }

      if (!NodeType.isElement(elm)) {
        return;
      }

      if (alignToTop === false) {
        offsetY = elm.offsetHeight;
      }

      if (root.nodeName !== 'BODY') {
        var scrollContainer = editor.selection.getScrollContainer();
        if (scrollContainer) {
          y = getPos(elm).y - getPos(scrollContainer).y + offsetY;
          viewPortH = scrollContainer.clientHeight;
          viewPortY = scrollContainer.scrollTop;
          if (y < viewPortY || y + 25 > viewPortY + viewPortH) {
            scrollContainer.scrollTop = y < viewPortY ? y : y - viewPortH + 25;
          }

          return;
        }
      }

      viewPort = dom.getViewPort(editor.getWin());
      y = dom.getPos(elm).y + offsetY;
      viewPortY = viewPort.y;
      viewPortH = viewPort.h;
      if (y < viewPort.y || y + 25 > viewPortY + viewPortH) {
        editor.getWin().scrollTo(0, y < viewPortY ? y : y - viewPortH + 25);
      }
    };

    return {
      scrollIntoView: scrollIntoView
    };
  }
);

/**
 * EventProcessRanges.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
 */

define(
  'tinymce.core.selection.EventProcessRanges',
  [
    'ephox.katamari.api.Arr'
  ],
  function (Arr) {
    var processRanges = function (editor, ranges) {
      return Arr.map(ranges, function (range) {
        var evt = editor.fire('GetSelectionRange', { range: range });
        return evt.range !== range ? evt.range : range;
      });
    };

    return {
      processRanges: processRanges
    };
  }
);



define(
  'ephox.sugar.api.dom.Replication',

  [
    'ephox.sugar.api.properties.Attr',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.dom.Insert',
    'ephox.sugar.api.dom.InsertAll',
    'ephox.sugar.api.dom.Remove',
    'ephox.sugar.api.search.Traverse'
  ],

  function (Attr, Element, Insert, InsertAll, Remove, Traverse) {
    var clone = function (original, deep) {
      return Element.fromDom(original.dom().cloneNode(deep));
    };

    /** Shallow clone - just the tag, no children */
    var shallow = function (original) {
      return clone(original, false);
    };

    /** Deep clone - everything copied including children */
    var deep = function (original) {
      return clone(original, true);
    };

    /** Shallow clone, with a new tag */
    var shallowAs = function (original, tag) {
      var nu = Element.fromTag(tag);

      var attributes = Attr.clone(original);
      Attr.setAll(nu, attributes);

      return nu;
    };

    /** Deep clone, with a new tag */
    var copy = function (original, tag) {
      var nu = shallowAs(original, tag);

      // NOTE
      // previously this used serialisation:
      // nu.dom().innerHTML = original.dom().innerHTML;
      //
      // Clone should be equivalent (and faster), but if TD <-> TH toggle breaks, put it back.

      var cloneChildren = Traverse.children(deep(original));
      InsertAll.append(nu, cloneChildren);

      return nu;
    };

    /** Change the tag name, but keep all children */
    var mutate = function (original, tag) {
      var nu = shallowAs(original, tag);

      Insert.before(original, nu);
      var children = Traverse.children(original);
      InsertAll.append(nu, children);
      Remove.remove(original);
      return nu;
    };

    return {
      shallow: shallow,
      shallowAs: shallowAs,
      deep: deep,
      copy: copy,
      mutate: mutate
    };
  }
);

define(
  'ephox.sugar.api.search.SelectorFind',

  [
    'ephox.sugar.api.search.PredicateFind',
    'ephox.sugar.api.search.Selectors',
    'ephox.sugar.impl.ClosestOrAncestor'
  ],

  function (PredicateFind, Selectors, ClosestOrAncestor) {
    // TODO: An internal SelectorFilter module that doesn't Element.fromDom() everything

    var first = function (selector) {
      return Selectors.one(selector);
    };

    var ancestor = function (scope, selector, isRoot) {
      return PredicateFind.ancestor(scope, function (e) {
        return Selectors.is(e, selector);
      }, isRoot);
    };

    var sibling = function (scope, selector) {
      return PredicateFind.sibling(scope, function (e) {
        return Selectors.is(e, selector);
      });
    };

    var child = function (scope, selector) {
      return PredicateFind.child(scope, function (e) {
        return Selectors.is(e, selector);
      });
    };

    var descendant = function (scope, selector) {
      return Selectors.one(selector, scope);
    };

    // Returns Some(closest ancestor element (sugared)) matching 'selector' up to isRoot, or None() otherwise
    var closest = function (scope, selector, isRoot) {
      return ClosestOrAncestor(Selectors.is, ancestor, scope, selector, isRoot);
    };

    return {
      first: first,
      ancestor: ancestor,
      sibling: sibling,
      child: child,
      descendant: descendant,
      closest: closest
    };
  }
);

/**
 * Parents.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
 */

define(
  'tinymce.core.dom.Parents',
  [
    'ephox.katamari.api.Fun',
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.search.Traverse'
  ],
  function (Fun, Compare, Traverse) {
    var dropLast = function (xs) {
      return xs.slice(0, -1);
    };

    var parentsUntil = function (startNode, rootElm, predicate) {
      if (Compare.contains(rootElm, startNode)) {
        return dropLast(Traverse.parents(startNode, function (elm) {
          return predicate(elm) || Compare.eq(elm, rootElm);
        }));
      } else {
        return [];
      }
    };

    var parents = function (startNode, rootElm) {
      return parentsUntil(startNode, rootElm, Fun.constant(false));
    };

    var parentsAndSelf = function (startNode, rootElm) {
      return [startNode].concat(parents(startNode, rootElm));
    };

    return {
      parentsUntil: parentsUntil,
      parents: parents,
      parentsAndSelf: parentsAndSelf
    };
  }
);

/**
 * SelectionUtils.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
 */

define(
  'tinymce.core.selection.SelectionUtils',
  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'ephox.katamari.api.Options',
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.node.Node',
    'ephox.sugar.api.search.Traverse',
    'tinymce.core.dom.NodeType'
  ],
  function (Arr, Fun, Option, Options, Compare, Element, Node, Traverse, NodeType) {
    var getStartNode = function (rng) {
      var sc = rng.startContainer, so = rng.startOffset;
      if (NodeType.isText(sc)) {
        return so === 0 ? Option.some(Element.fromDom(sc)) : Option.none();
      } else {
        return Option.from(sc.childNodes[so]).map(Element.fromDom);
      }
    };

    var getEndNode = function (rng) {
      var ec = rng.endContainer, eo = rng.endOffset;
      if (NodeType.isText(ec)) {
        return eo === ec.data.length ? Option.some(Element.fromDom(ec)) : Option.none();
      } else {
        return Option.from(ec.childNodes[eo - 1]).map(Element.fromDom);
      }
    };

    var getFirstChildren = function (node) {
      return Traverse.firstChild(node).fold(
        Fun.constant([node]),
        function (child) {
          return [node].concat(getFirstChildren(child));
        }
      );
    };

    var getLastChildren = function (node) {
      return Traverse.lastChild(node).fold(
        Fun.constant([node]),
        function (child) {
          if (Node.name(child) === 'br') {
            return Traverse.prevSibling(child).map(function (sibling) {
              return [node].concat(getLastChildren(sibling));
            }).getOr([]);
          } else {
            return [node].concat(getLastChildren(child));
          }
        }
      );
    };

    var hasAllContentsSelected = function (elm, rng) {
      return Options.liftN([getStartNode(rng), getEndNode(rng)], function (startNode, endNode) {
        var start = Arr.find(getFirstChildren(elm), Fun.curry(Compare.eq, startNode));
        var end = Arr.find(getLastChildren(elm), Fun.curry(Compare.eq, endNode));
        return start.isSome() && end.isSome();
      }).getOr(false);
    };

    return {
      hasAllContentsSelected: hasAllContentsSelected
    };
  }
);

/**
 * SimpleTableModel.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
 */

define(
  'tinymce.core.selection.SimpleTableModel',
  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Option',
    'ephox.katamari.api.Struct',
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.dom.Insert',
    'ephox.sugar.api.dom.InsertAll',
    'ephox.sugar.api.dom.Replication',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.properties.Attr',
    'ephox.sugar.api.search.SelectorFilter'
  ],
  function (Arr, Option, Struct, Compare, Insert, InsertAll, Replication, Element, Attr, SelectorFilter) {
    var tableModel = Struct.immutable('element', 'width', 'rows');
    var tableRow = Struct.immutable('element', 'cells');
    var cellPosition = Struct.immutable('x', 'y');

    var getSpan = function (td, key) {
      var value = parseInt(Attr.get(td, key), 10);
      return isNaN(value) ? 1 : value;
    };

    var fillout = function (table, x, y, tr, td) {
      var rowspan = getSpan(td, 'rowspan');
      var colspan = getSpan(td, 'colspan');
      var rows = table.rows();

      for (var y2 = y; y2 < y + rowspan; y2++) {
        if (!rows[y2]) {
          rows[y2] = tableRow(Replication.deep(tr), []);
        }

        for (var x2 = x; x2 < x + colspan; x2++) {
          var cells = rows[y2].cells();

          // not filler td:s are purposely not cloned so that we can
          // find cells in the model by element object references
          cells[x2] = y2 == y && x2 == x ? td : Replication.shallow(td);
        }
      }
    };

    var cellExists = function (table, x, y) {
      var rows = table.rows();
      var cells = rows[y] ? rows[y].cells() : [];
      return !!cells[x];
    };

    var skipCellsX = function (table, x, y) {
      while (cellExists(table, x, y)) {
        x++;
      }

      return x;
    };

    var getWidth = function (rows) {
      return Arr.foldl(rows, function (acc, row) {
        return row.cells().length > acc ? row.cells().length : acc;
      }, 0);
    };

    var findElementPos = function (table, element) {
      var rows = table.rows();
      for (var y = 0; y < rows.length; y++) {
        var cells = rows[y].cells();
        for (var x = 0; x < cells.length; x++) {
          if (Compare.eq(cells[x], element)) {
            return Option.some(cellPosition(x, y));
          }
        }
      }

      return Option.none();
    };

    var extractRows = function (table, sx, sy, ex, ey) {
      var newRows = [];
      var rows = table.rows();

      for (var y = sy; y <= ey; y++) {
        var cells = rows[y].cells();
        var slice = sx < ex ? cells.slice(sx, ex + 1) : cells.slice(ex, sx + 1);
        newRows.push(tableRow(rows[y].element(), slice));
      }

      return newRows;
    };

    var subTable = function (table, startPos, endPos) {
      var sx = startPos.x(), sy = startPos.y();
      var ex = endPos.x(), ey = endPos.y();
      var newRows = sy < ey ? extractRows(table, sx, sy, ex, ey) : extractRows(table, sx, ey, ex, sy);

      return tableModel(table.element(), getWidth(newRows), newRows);
    };

    var createDomTable = function (table, rows) {
      var tableElement = Replication.shallow(table.element());
      var tableBody = Element.fromTag('tbody');

      InsertAll.append(tableBody, rows);
      Insert.append(tableElement, tableBody);

      return tableElement;
    };

    var modelRowsToDomRows = function (table) {
      return Arr.map(table.rows(), function (row) {
        var cells = Arr.map(row.cells(), function (cell) {
          var td = Replication.deep(cell);
          Attr.remove(td, 'colspan');
          Attr.remove(td, 'rowspan');
          return td;
        });

        var tr = Replication.shallow(row.element());
        InsertAll.append(tr, cells);
        return tr;
      });
    };

    var fromDom = function (tableElm) {
      var table = tableModel(Replication.shallow(tableElm), 0, []);

      Arr.each(SelectorFilter.descendants(tableElm, 'tr'), function (tr, y) {
        Arr.each(SelectorFilter.descendants(tr, 'td,th'), function (td, x) {
          fillout(table, skipCellsX(table, x, y), y, tr, td);
        });
      });

      return tableModel(table.element(), getWidth(table.rows()), table.rows());
    };

    var toDom = function (table) {
      return createDomTable(table, modelRowsToDomRows(table));
    };

    var subsection = function (table, startElement, endElement) {
      return findElementPos(table, startElement).bind(function (startPos) {
        return findElementPos(table, endElement).map(function (endPos) {
          return subTable(table, startPos, endPos);
        });
      });
    };

    return {
      fromDom: fromDom,
      toDom: toDom,
      subsection: subsection
    };
  }
);

/**
 * MultiRange.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
 */

define(
  'tinymce.core.selection.MultiRange',
  [
    'ephox.katamari.api.Arr',
    'ephox.sugar.api.node.Element',
    'tinymce.core.dom.RangeUtils'
  ],
  function (Arr, Element, RangeUtils) {
    var getRanges = function (selection) {
      var ranges = [];

      for (var i = 0; i < selection.rangeCount; i++) {
        ranges.push(selection.getRangeAt(i));
      }

      return ranges;
    };

    var getSelectedNodes = function (ranges) {
      return Arr.bind(ranges, function (range) {
        var node = RangeUtils.getSelectedNode(range);
        return node ? [ Element.fromDom(node) ] : [];
      });
    };

    var hasMultipleRanges = function (selection) {
      return getRanges(selection).length > 1;
    };

    return {
      getRanges: getRanges,
      getSelectedNodes: getSelectedNodes,
      hasMultipleRanges: hasMultipleRanges
    };
  }
);

/**
 * TableCellSelection.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
 */

define(
  'tinymce.core.selection.TableCellSelection',
  [
    'ephox.katamari.api.Arr',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.search.SelectorFilter',
    'tinymce.core.dom.ElementType',
    'tinymce.core.selection.MultiRange'
  ],
  function (Arr, Element, SelectorFilter, ElementType, MultiRange) {
    var getCellsFromRanges = function (ranges) {
      return Arr.filter(MultiRange.getSelectedNodes(ranges), ElementType.isTableCell);
    };

    var getCellsFromElement = function (elm) {
      var selectedCells = SelectorFilter.descendants(elm, 'td[data-mce-selected],th[data-mce-selected]');
      return selectedCells;
    };

    var getCellsFromElementOrRanges = function (ranges, element) {
      var selectedCells = getCellsFromElement(element);
      var rangeCells = getCellsFromRanges(ranges);
      return selectedCells.length > 0 ? selectedCells : rangeCells;
    };

    var getCellsFromEditor = function (editor) {
      return getCellsFromElementOrRanges(MultiRange.getRanges(editor.selection.getSel()), Element.fromDom(editor.getBody()));
    };

    return {
      getCellsFromRanges: getCellsFromRanges,
      getCellsFromElement: getCellsFromElement,
      getCellsFromElementOrRanges: getCellsFromElementOrRanges,
      getCellsFromEditor: getCellsFromEditor
    };
  }
);

/**
 * FragmentReader.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
 */

define(
  'tinymce.core.selection.FragmentReader',
  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Fun',
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.dom.Insert',
    'ephox.sugar.api.dom.Replication',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.node.Fragment',
    'ephox.sugar.api.node.Node',
    'ephox.sugar.api.search.SelectorFind',
    'ephox.sugar.api.search.Traverse',
    'tinymce.core.dom.ElementType',
    'tinymce.core.dom.Parents',
    'tinymce.core.selection.SelectionUtils',
    'tinymce.core.selection.SimpleTableModel',
    'tinymce.core.selection.TableCellSelection'
  ],
  function (Arr, Fun, Compare, Insert, Replication, Element, Fragment, Node, SelectorFind, Traverse, ElementType, Parents, SelectionUtils, SimpleTableModel, TableCellSelection) {
    var findParentListContainer = function (parents) {
      return Arr.find(parents, function (elm) {
        return Node.name(elm) === 'ul' || Node.name(elm) === 'ol';
      });
    };

    var getFullySelectedListWrappers = function (parents, rng) {
      return Arr.find(parents, function (elm) {
        return Node.name(elm) === 'li' && SelectionUtils.hasAllContentsSelected(elm, rng);
      }).fold(
        Fun.constant([]),
        function (li) {
          return findParentListContainer(parents).map(function (listCont) {
            return [
              Element.fromTag('li'),
              Element.fromTag(Node.name(listCont))
            ];
          }).getOr([]);
        }
      );
    };

    var wrap = function (innerElm, elms) {
      var wrapped = Arr.foldl(elms, function (acc, elm) {
        Insert.append(elm, acc);
        return elm;
      }, innerElm);
      return elms.length > 0 ? Fragment.fromElements([wrapped]) : wrapped;
    };

    var directListWrappers = function (commonAnchorContainer) {
      if (ElementType.isListItem(commonAnchorContainer)) {
        return Traverse.parent(commonAnchorContainer).filter(ElementType.isList).fold(
          Fun.constant([]),
          function (listElm) {
            return [ commonAnchorContainer, listElm ];
          }
        );
      } else {
        return ElementType.isList(commonAnchorContainer) ? [ commonAnchorContainer ] : [ ];
      }
    };

    var getWrapElements = function (rootNode, rng) {
      var commonAnchorContainer = Element.fromDom(rng.commonAncestorContainer);
      var parents = Parents.parentsAndSelf(commonAnchorContainer, rootNode);
      var wrapElements = Arr.filter(parents, function (elm) {
        return ElementType.isInline(elm) || ElementType.isHeading(elm);
      });
      var listWrappers = getFullySelectedListWrappers(parents, rng);
      var allWrappers = wrapElements.concat(listWrappers.length ? listWrappers : directListWrappers(commonAnchorContainer));
      return Arr.map(allWrappers, Replication.shallow);
    };

    var emptyFragment = function () {
      return Fragment.fromElements([]);
    };

    var getFragmentFromRange = function (rootNode, rng) {
      return wrap(Element.fromDom(rng.cloneContents()), getWrapElements(rootNode, rng));
    };

    var getParentTable = function (rootElm, cell) {
      return SelectorFind.ancestor(cell, 'table', Fun.curry(Compare.eq, rootElm));
    };

    var getTableFragment = function (rootNode, selectedTableCells) {
      return getParentTable(rootNode, selectedTableCells[0]).bind(function (tableElm) {
        var firstCell = selectedTableCells[0];
        var lastCell = selectedTableCells[selectedTableCells.length - 1];
        var fullTableModel = SimpleTableModel.fromDom(tableElm);

        return SimpleTableModel.subsection(fullTableModel, firstCell, lastCell).map(function (sectionedTableModel) {
          return Fragment.fromElements([SimpleTableModel.toDom(sectionedTableModel)]);
        });
      }).getOrThunk(emptyFragment);
    };

    var getSelectionFragment = function (rootNode, ranges) {
      return ranges.length > 0 && ranges[0].collapsed ? emptyFragment() : getFragmentFromRange(rootNode, ranges[0]);
    };

    var read = function (rootNode, ranges) {
      var selectedCells = TableCellSelection.getCellsFromElementOrRanges(ranges, rootNode);
      return selectedCells.length > 0 ? getTableFragment(rootNode, selectedCells) : getSelectionFragment(rootNode, ranges);
    };

    return {
      read: read
    };
  }
);

/**
 * GetSelectionContent.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
 */

define(
  'tinymce.core.selection.GetSelectionContent',
  [
    'ephox.sugar.api.node.Element',
    'tinymce.core.selection.EventProcessRanges',
    'tinymce.core.selection.FragmentReader',
    'tinymce.core.selection.MultiRange',
    'tinymce.core.text.Zwsp'
  ],
  function (Element, EventProcessRanges, FragmentReader, MultiRange, Zwsp) {
    var getContent = function (editor, args) {
      var rng = editor.selection.getRng(), tmpElm = editor.dom.create("body");
      var sel = editor.selection.getSel(), whiteSpaceBefore, whiteSpaceAfter, fragment;
      var ranges = EventProcessRanges.processRanges(editor, MultiRange.getRanges(sel));

      args = args || {};
      whiteSpaceBefore = whiteSpaceAfter = '';
      args.get = true;
      args.format = args.format || 'html';
      args.selection = true;

      args = editor.fire('BeforeGetContent', args);
      if (args.isDefaultPrevented()) {
        editor.fire('GetContent', args);
        return args.content;
      }

      if (args.format === 'text') {
        return editor.selection.isCollapsed() ? '' : Zwsp.trim(rng.text || (sel.toString ? sel.toString() : ''));
      }

      if (rng.cloneContents) {
        fragment = args.contextual ? FragmentReader.read(Element.fromDom(editor.getBody()), ranges).dom() : rng.cloneContents();
        if (fragment) {
          tmpElm.appendChild(fragment);
        }
      } else if (rng.item !== undefined || rng.htmlText !== undefined) {
        // IE will produce invalid markup if elements are present that
        // it doesn't understand like custom elements or HTML5 elements.
        // Adding a BR in front of the contents and then remoiving it seems to fix it though.
        tmpElm.innerHTML = '<br>' + (rng.item ? rng.item(0).outerHTML : rng.htmlText);
        tmpElm.removeChild(tmpElm.firstChild);
      } else {
        tmpElm.innerHTML = rng.toString();
      }

      // Keep whitespace before and after
      if (/^\s/.test(tmpElm.innerHTML)) {
        whiteSpaceBefore = ' ';
      }

      if (/\s+$/.test(tmpElm.innerHTML)) {
        whiteSpaceAfter = ' ';
      }

      args.getInner = true;

      args.content = editor.selection.isCollapsed() ? '' : whiteSpaceBefore + editor.selection.serializer.serialize(tmpElm, args) + whiteSpaceAfter;
      editor.fire('GetContent', args);

      return args.content;
    };

    return {
      getContent: getContent
    };
  }
);

/**
 * SetSelectionContent.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
 */

define(
  'tinymce.core.selection.SetSelectionContent',
  [
  ],
  function () {
    var setContent = function (editor, content, args) {
      var rng = editor.selection.getRng(), caretNode, doc = editor.getDoc(), frag, temp;

      args = args || { format: 'html' };
      args.set = true;
      args.selection = true;
      args.content = content;

      // Dispatch before set content event
      if (!args.no_events) {
        args = editor.fire('BeforeSetContent', args);
        if (args.isDefaultPrevented()) {
          editor.fire('SetContent', args);
          return;
        }
      }

      content = args.content;

      if (rng.insertNode) {
        // Make caret marker since insertNode places the caret in the beginning of text after insert
        content += '<span id="__caret">_</span>';

        // Delete and insert new node
        if (rng.startContainer == doc && rng.endContainer == doc) {
          // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents
          doc.body.innerHTML = content;
        } else {
          rng.deleteContents();

          if (doc.body.childNodes.length === 0) {
            doc.body.innerHTML = content;
          } else {
            // createContextualFragment doesn't exists in IE 9 DOMRanges
            if (rng.createContextualFragment) {
              rng.insertNode(rng.createContextualFragment(content));
            } else {
              // Fake createContextualFragment call in IE 9
              frag = doc.createDocumentFragment();
              temp = doc.createElement('div');

              frag.appendChild(temp);
              temp.outerHTML = content;

              rng.insertNode(frag);
            }
          }
        }

        // Move to caret marker
        caretNode = editor.dom.get('__caret');

        // Make sure we wrap it compleatly, Opera fails with a simple select call
        rng = doc.createRange();
        rng.setStartBefore(caretNode);
        rng.setEndBefore(caretNode);
        editor.selection.setRng(rng);

        // Remove the caret position
        editor.dom.remove('__caret');

        try {
          editor.selection.setRng(rng);
        } catch (ex) {
          // Might fail on Opera for some odd reason
        }
      } else {
        if (rng.item) {
          // Delete content and get caret text selection
          doc.execCommand('Delete', false, null);
          rng = editor.getRng();
        }

        // Explorer removes spaces from the beginning of pasted contents
        if (/^\s+/.test(content)) {
          rng.pasteHTML('<span id="__mce_tmp">_</span>' + content);
          editor.dom.remove('__mce_tmp');
        } else {
          rng.pasteHTML(content);
        }
      }

      // Dispatch set content event
      if (!args.no_events) {
        editor.fire('SetContent', args);
      }
    };

    return {
      setContent: setContent
    };
  }
);

/**
 * Selection.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
 */

/**
 * This class handles text and control selection it's an crossbrowser utility class.
 * Consult the TinyMCE Wiki API for more details and examples on how to use this class.
 *
 * @class tinymce.dom.Selection
 * @example
 * // Getting the currently selected node for the active editor
 * alert(tinymce.activeEditor.selection.getNode().nodeName);
 */
define(
  'tinymce.core.dom.Selection',
  [
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.node.Element',
    'tinymce.core.EditorFocus',
    'tinymce.core.Env',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.dom.BookmarkManager',
    'tinymce.core.dom.ControlSelection',
    'tinymce.core.dom.RangeUtils',
    'tinymce.core.dom.ScrollIntoView',
    'tinymce.core.dom.TreeWalker',
    'tinymce.core.selection.EventProcessRanges',
    'tinymce.core.selection.GetSelectionContent',
    'tinymce.core.selection.MultiRange',
    'tinymce.core.selection.SelectionBookmark',
    'tinymce.core.selection.SetSelectionContent',
    'tinymce.core.util.Tools'
  ],
  function (
    Compare, Element, EditorFocus, Env, CaretPosition, BookmarkManager, ControlSelection, RangeUtils, ScrollIntoView, TreeWalker, EventProcessRanges, GetSelectionContent,
    MultiRange, SelectionBookmark, SetSelectionContent, Tools
  ) {
    var each = Tools.each, trim = Tools.trim;

    var isAttachedToDom = function (node) {
      return !!(node && node.ownerDocument) && Compare.contains(Element.fromDom(node.ownerDocument), Element.fromDom(node));
    };

    var isValidRange = function (rng) {
      if (!rng) {
        return false;
      } else if (rng.select) { // Native IE range still produced by placeCaretAt
        return true;
      } else {
        return isAttachedToDom(rng.startContainer) && isAttachedToDom(rng.endContainer);
      }
    };

    /**
     * Constructs a new selection instance.
     *
     * @constructor
     * @method Selection
     * @param {tinymce.dom.DOMUtils} dom DOMUtils object reference.
     * @param {Window} win Window to bind the selection object to.
     * @param {tinymce.Editor} editor Editor instance of the selection.
     * @param {tinymce.dom.Serializer} serializer DOM serialization class to use for getContent.
     */
    var Selection = function (dom, win, serializer, editor) {
      var self = this;

      self.dom = dom;
      self.win = win;
      self.serializer = serializer;
      self.editor = editor;
      self.bookmarkManager = new BookmarkManager(self);
      self.controlSelection = new ControlSelection(self, editor);
    };

    Selection.prototype = {
      /**
       * Move the selection cursor range to the specified node and offset.
       * If there is no node specified it will move it to the first suitable location within the body.
       *
       * @method setCursorLocation
       * @param {Node} node Optional node to put the cursor in.
       * @param {Number} offset Optional offset from the start of the node to put the cursor at.
       */
      setCursorLocation: function (node, offset) {
        var self = this, rng = self.dom.createRng();

        if (!node) {
          self._moveEndPoint(rng, self.editor.getBody(), true);
          self.setRng(rng);
        } else {
          rng.setStart(node, offset);
          rng.setEnd(node, offset);
          self.setRng(rng);
          self.collapse(false);
        }
      },

      /**
       * Returns the selected contents using the DOM serializer passed in to this class.
       *
       * @method getContent
       * @param {Object} args Optional settings class with for example output format text or html.
       * @return {String} Selected contents in for example HTML format.
       * @example
       * // Alerts the currently selected contents
       * alert(tinymce.activeEditor.selection.getContent());
       *
       * // Alerts the currently selected contents as plain text
       * alert(tinymce.activeEditor.selection.getContent({format: 'text'}));
       */
      getContent: function (args) {
        return GetSelectionContent.getContent(this.editor, args);
      },

      /**
       * Sets the current selection to the specified content. If any contents is selected it will be replaced
       * with the contents passed in to this function. If there is no selection the contents will be inserted
       * where the caret is placed in the editor/page.
       *
       * @method setContent
       * @param {String} content HTML contents to set could also be other formats depending on settings.
       * @param {Object} args Optional settings object with for example data format.
       * @example
       * // Inserts some HTML contents at the current selection
       * tinymce.activeEditor.selection.setContent('<strong>Some contents</strong>');
       */
      setContent: function (content, args) {
        SetSelectionContent.setContent(this.editor, content, args);
      },

      /**
       * Returns the start element of a selection range. If the start is in a text
       * node the parent element will be returned.
       *
       * @method getStart
       * @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element.
       * @return {Element} Start element of selection range.
       */
      getStart: function (real) {
        var self = this, rng = self.getRng(), startElement;

        startElement = rng.startContainer;

        if (startElement.nodeType == 1 && startElement.hasChildNodes()) {
          if (!real || !rng.collapsed) {
            startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)];
          }
        }

        if (startElement && startElement.nodeType == 3) {
          return startElement.parentNode;
        }

        return startElement;
      },

      /**
       * Returns the end element of a selection range. If the end is in a text
       * node the parent element will be returned.
       *
       * @method getEnd
       * @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element.
       * @return {Element} End element of selection range.
       */
      getEnd: function (real) {
        var self = this, rng = self.getRng(), endElement, endOffset;

        endElement = rng.endContainer;
        endOffset = rng.endOffset;

        if (endElement.nodeType == 1 && endElement.hasChildNodes()) {
          if (!real || !rng.collapsed) {
            endElement = endElement.childNodes[endOffset > 0 ? endOffset - 1 : endOffset];
          }
        }

        if (endElement && endElement.nodeType == 3) {
          return endElement.parentNode;
        }

        return endElement;
      },

      /**
       * Returns a bookmark location for the current selection. This bookmark object
       * can then be used to restore the selection after some content modification to the document.
       *
       * @method getBookmark
       * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex.
       * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization.
       * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection.
       * @example
       * // Stores a bookmark of the current selection
       * var bm = tinymce.activeEditor.selection.getBookmark();
       *
       * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
       *
       * // Restore the selection bookmark
       * tinymce.activeEditor.selection.moveToBookmark(bm);
       */
      getBookmark: function (type, normalized) {
        return this.bookmarkManager.getBookmark(type, normalized);
      },

      /**
       * Restores the selection to the specified bookmark.
       *
       * @method moveToBookmark
       * @param {Object} bookmark Bookmark to restore selection from.
       * @return {Boolean} true/false if it was successful or not.
       * @example
       * // Stores a bookmark of the current selection
       * var bm = tinymce.activeEditor.selection.getBookmark();
       *
       * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
       *
       * // Restore the selection bookmark
       * tinymce.activeEditor.selection.moveToBookmark(bm);
       */
      moveToBookmark: function (bookmark) {
        return this.bookmarkManager.moveToBookmark(bookmark);
      },

      /**
       * Selects the specified element. This will place the start and end of the selection range around the element.
       *
       * @method select
       * @param {Element} node HTML DOM element to select.
       * @param {Boolean} content Optional bool state if the contents should be selected or not on non IE browser.
       * @return {Element} Selected element the same element as the one that got passed in.
       * @example
       * // Select the first paragraph in the active editor
       * tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]);
       */
      select: function (node, content) {
        var self = this, dom = self.dom, rng = dom.createRng(), idx;

        if (node) {
          if (!content && self.controlSelection.controlSelect(node)) {
            return;
          }

          idx = dom.nodeIndex(node);
          rng.setStart(node.parentNode, idx);
          rng.setEnd(node.parentNode, idx + 1);

          // Find first/last text node or BR element
          if (content) {
            self._moveEndPoint(rng, node, true);
            self._moveEndPoint(rng, node);
          }

          self.setRng(rng);
        }

        return node;
      },

      /**
       * Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection.
       *
       * @method isCollapsed
       * @return {Boolean} true/false state if the selection range is collapsed or not.
       * Collapsed means if it's a caret or a larger selection.
       */
      isCollapsed: function () {
        var self = this, rng = self.getRng(), sel = self.getSel();

        if (!rng || rng.item) {
          return false;
        }

        if (rng.compareEndPoints) {
          return rng.compareEndPoints('StartToEnd', rng) === 0;
        }

        return !sel || rng.collapsed;
      },

      /**
       * Collapse the selection to start or end of range.
       *
       * @method collapse
       * @param {Boolean} toStart Optional boolean state if to collapse to end or not. Defaults to false.
       */
      collapse: function (toStart) {
        var self = this, rng = self.getRng();

        rng.collapse(!!toStart);
        self.setRng(rng);
      },

      /**
       * Returns the browsers internal selection object.
       *
       * @method getSel
       * @return {Selection} Internal browser selection object.
       */
      getSel: function () {
        var win = this.win;

        return win.getSelection ? win.getSelection() : win.document.selection;
      },

      /**
       * Returns the browsers internal range object.
       *
       * @method getRng
       * @param {Boolean} w3c Forces a compatible W3C range on IE.
       * @return {Range} Internal browser range object.
       * @see http://www.quirksmode.org/dom/range_intro.html
       * @see http://www.dotvoid.com/2001/03/using-the-range-object-in-mozilla/
       */
      getRng: function (w3c) {
        var self = this, selection, rng, elm, doc;

        var tryCompareBoundaryPoints = function (how, sourceRange, destinationRange) {
          try {
            return sourceRange.compareBoundaryPoints(how, destinationRange);
          } catch (ex) {
            // Gecko throws wrong document exception if the range points
            // to nodes that where removed from the dom #6690
            // Browsers should mutate existing DOMRange instances so that they always point
            // to something in the document this is not the case in Gecko works fine in IE/WebKit/Blink
            // For performance reasons just return -1
            return -1;
          }
        };

        if (!self.win) {
          return null;
        }

        doc = self.win.document;

        if (typeof doc === 'undefined' || doc === null) {
          return null;
        }

        if (self.editor.bookmark !== undefined && EditorFocus.hasFocus(self.editor) === false) {
          var bookmark = SelectionBookmark.getRng(self.editor);

          if (bookmark.isSome()) {
            return bookmark.getOr(doc.createRange());
          }
        }

        try {
          if ((selection = self.getSel())) {
            if (selection.rangeCount > 0) {
              rng = selection.getRangeAt(0);
            } else {
              rng = selection.createRange ? selection.createRange() : doc.createRange();
            }
          }
        } catch (ex) {
          // IE throws unspecified error here if TinyMCE is placed in a frame/iframe
        }

        rng = EventProcessRanges.processRanges(self.editor, [rng])[0];

        // No range found then create an empty one
        // This can occur when the editor is placed in a hidden container element on Gecko
        // Or on IE when there was an exception
        if (!rng) {
          rng = doc.createRange ? doc.createRange() : doc.body.createTextRange();
        }

        // If range is at start of document then move it to start of body
        if (rng.setStart && rng.startContainer.nodeType === 9 && rng.collapsed) {
          elm = self.dom.getRoot();
          rng.setStart(elm, 0);
          rng.setEnd(elm, 0);
        }

        if (self.selectedRange && self.explicitRange) {
          if (tryCompareBoundaryPoints(rng.START_TO_START, rng, self.selectedRange) === 0 &&
            tryCompareBoundaryPoints(rng.END_TO_END, rng, self.selectedRange) === 0) {
            // Safari, Opera and Chrome only ever select text which causes the range to change.
            // This lets us use the originally set range if the selection hasn't been changed by the user.
            rng = self.explicitRange;
          } else {
            self.selectedRange = null;
            self.explicitRange = null;
          }
        }

        return rng;
      },

      /**
       * Changes the selection to the specified DOM range.
       *
       * @method setRng
       * @param {Range} rng Range to select.
       * @param {Boolean} forward Optional boolean if the selection is forwards or backwards.
       */
      setRng: function (rng, forward) {
        var self = this, sel, node, evt;

        if (!isValidRange(rng)) {
          return;
        }

        // Is IE specific range
        if (rng.select) {
          self.explicitRange = null;

          try {
            rng.select();
          } catch (ex) {
            // Needed for some odd IE bug #1843306
          }

          return;
        }

        sel = self.getSel();

        evt = self.editor.fire('SetSelectionRange', { range: rng, forward: forward });
        rng = evt.range;

        if (sel) {
          self.explicitRange = rng;

          try {
            sel.removeAllRanges();
            sel.addRange(rng);
          } catch (ex) {
            // IE might throw errors here if the editor is within a hidden container and selection is changed
          }

          // Forward is set to false and we have an extend function
          if (forward === false && sel.extend) {
            sel.collapse(rng.endContainer, rng.endOffset);
            sel.extend(rng.startContainer, rng.startOffset);
          }

          // adding range isn't always successful so we need to check range count otherwise an exception can occur
          self.selectedRange = sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
        }

        // WebKit egde case selecting images works better using setBaseAndExtent when the image is floated
        if (!rng.collapsed && rng.startContainer === rng.endContainer && sel.setBaseAndExtent && !Env.ie) {
          if (rng.endOffset - rng.startOffset < 2) {
            if (rng.startContainer.hasChildNodes()) {
              node = rng.startContainer.childNodes[rng.startOffset];
              if (node && node.tagName === 'IMG') {
                sel.setBaseAndExtent(
                  rng.startContainer,
                  rng.startOffset,
                  rng.endContainer,
                  rng.endOffset
                );

                // Since the setBaseAndExtent is fixed in more recent Blink versions we
                // need to detect if it's doing the wrong thing and falling back to the
                // crazy incorrect behavior api call since that seems to be the only way
                // to get it to work on Safari WebKit as of 2017-02-23
                if (sel.anchorNode !== rng.startContainer || sel.focusNode !== rng.endContainer) {
                  sel.setBaseAndExtent(node, 0, node, 1);
                }
              }
            }
          }
        }

        self.editor.fire('AfterSetSelectionRange', { range: rng, forward: forward });
      },

      /**
       * Sets the current selection to the specified DOM element.
       *
       * @method setNode
       * @param {Element} elm Element to set as the contents of the selection.
       * @return {Element} Returns the element that got passed in.
       * @example
       * // Inserts a DOM node at current selection/caret location
       * tinymce.activeEditor.selection.setNode(tinymce.activeEditor.dom.create('img', {src: 'some.gif', title: 'some title'}));
       */
      setNode: function (elm) {
        var self = this;

        self.setContent(self.dom.getOuterHTML(elm));

        return elm;
      },

      /**
       * Returns the currently selected element or the common ancestor element for both start and end of the selection.
       *
       * @method getNode
       * @return {Element} Currently selected element or common ancestor element.
       * @example
       * // Alerts the currently selected elements node name
       * alert(tinymce.activeEditor.selection.getNode().nodeName);
       */
      getNode: function () {
        var self = this, rng = self.getRng(), elm;
        var startContainer, endContainer, startOffset, endOffset, root = self.dom.getRoot();

        var skipEmptyTextNodes = function (node, forwards) {
          var orig = node;

          while (node && node.nodeType === 3 && node.length === 0) {
            node = forwards ? node.nextSibling : node.previousSibling;
          }

          return node || orig;
        };

        // Range maybe lost after the editor is made visible again
        if (!rng) {
          return root;
        }

        startContainer = rng.startContainer;
        endContainer = rng.endContainer;
        startOffset = rng.startOffset;
        endOffset = rng.endOffset;
        elm = rng.commonAncestorContainer;

        // Handle selection a image or other control like element such as anchors
        if (!rng.collapsed) {
          if (startContainer == endContainer) {
            if (endOffset - startOffset < 2) {
              if (startContainer.hasChildNodes()) {
                elm = startContainer.childNodes[startOffset];
              }
            }
          }

          // If the anchor node is a element instead of a text node then return this element
          //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1)
          // return sel.anchorNode.childNodes[sel.anchorOffset];

          // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent.
          // This happens when you double click an underlined word in FireFox.
          if (startContainer.nodeType === 3 && endContainer.nodeType === 3) {
            if (startContainer.length === startOffset) {
              startContainer = skipEmptyTextNodes(startContainer.nextSibling, true);
            } else {
              startContainer = startContainer.parentNode;
            }

            if (endOffset === 0) {
              endContainer = skipEmptyTextNodes(endContainer.previousSibling, false);
            } else {
              endContainer = endContainer.parentNode;
            }

            if (startContainer && startContainer === endContainer) {
              return startContainer;
            }
          }
        }

        if (elm && elm.nodeType == 3) {
          return elm.parentNode;
        }

        return elm;
      },

      getSelectedBlocks: function (startElm, endElm) {
        var self = this, dom = self.dom, node, root, selectedBlocks = [];

        root = dom.getRoot();
        startElm = dom.getParent(startElm || self.getStart(), dom.isBlock);
        endElm = dom.getParent(endElm || self.getEnd(), dom.isBlock);

        if (startElm && startElm != root) {
          selectedBlocks.push(startElm);
        }

        if (startElm && endElm && startElm != endElm) {
          node = startElm;

          var walker = new TreeWalker(startElm, root);
          while ((node = walker.next()) && node != endElm) {
            if (dom.isBlock(node)) {
              selectedBlocks.push(node);
            }
          }
        }

        if (endElm && startElm != endElm && endElm != root) {
          selectedBlocks.push(endElm);
        }

        return selectedBlocks;
      },

      isForward: function () {
        var dom = this.dom, sel = this.getSel(), anchorRange, focusRange;

        // No support for selection direction then always return true
        if (!sel || !sel.anchorNode || !sel.focusNode) {
          return true;
        }

        anchorRange = dom.createRng();
        anchorRange.setStart(sel.anchorNode, sel.anchorOffset);
        anchorRange.collapse(true);

        focusRange = dom.createRng();
        focusRange.setStart(sel.focusNode, sel.focusOffset);
        focusRange.collapse(true);

        return anchorRange.compareBoundaryPoints(anchorRange.START_TO_START, focusRange) <= 0;
      },

      normalize: function () {
        var self = this, rng = self.getRng();

        if (new RangeUtils(self.dom).normalize(rng) && !MultiRange.hasMultipleRanges(self.getSel())) {
          self.setRng(rng, self.isForward());
        }

        return rng;
      },

      /**
       * Executes callback when the current selection starts/stops matching the specified selector. The current
       * state will be passed to the callback as it's first argument.
       *
       * @method selectorChanged
       * @param {String} selector CSS selector to check for.
       * @param {function} callback Callback with state and args when the selector is matches or not.
       */
      selectorChanged: function (selector, callback) {
        var self = this, currentSelectors;

        if (!self.selectorChangedData) {
          self.selectorChangedData = {};
          currentSelectors = {};

          self.editor.on('NodeChange', function (e) {
            var node = e.element, dom = self.dom, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {};

            // Check for new matching selectors
            each(self.selectorChangedData, function (callbacks, selector) {
              each(parents, function (node) {
                if (dom.is(node, selector)) {
                  if (!currentSelectors[selector]) {
                    // Execute callbacks
                    each(callbacks, function (callback) {
                      callback(true, { node: node, selector: selector, parents: parents });
                    });

                    currentSelectors[selector] = callbacks;
                  }

                  matchedSelectors[selector] = callbacks;
                  return false;
                }
              });
            });

            // Check if current selectors still match
            each(currentSelectors, function (callbacks, selector) {
              if (!matchedSelectors[selector]) {
                delete currentSelectors[selector];

                each(callbacks, function (callback) {
                  callback(false, { node: node, selector: selector, parents: parents });
                });
              }
            });
          });
        }

        // Add selector listeners
        if (!self.selectorChangedData[selector]) {
          self.selectorChangedData[selector] = [];
        }

        self.selectorChangedData[selector].push(callback);

        return self;
      },

      getScrollContainer: function () {
        var scrollContainer, node = this.dom.getRoot();

        while (node && node.nodeName != 'BODY') {
          if (node.scrollHeight > node.clientHeight) {
            scrollContainer = node;
            break;
          }

          node = node.parentNode;
        }

        return scrollContainer;
      },

      scrollIntoView: function (elm, alignToTop) {
        ScrollIntoView.scrollIntoView(this.editor, elm, alignToTop);
      },

      placeCaretAt: function (clientX, clientY) {
        this.setRng(RangeUtils.getCaretRangeFromPoint(clientX, clientY, this.editor.getDoc()));
      },

      _moveEndPoint: function (rng, node, start) {
        var root = node, walker = new TreeWalker(node, root);
        var nonEmptyElementsMap = this.dom.schema.getNonEmptyElements();

        do {
          // Text node
          if (node.nodeType == 3 && trim(node.nodeValue).length !== 0) {
            if (start) {
              rng.setStart(node, 0);
            } else {
              rng.setEnd(node, node.nodeValue.length);
            }

            return;
          }

          // BR/IMG/INPUT elements but not table cells
          if (nonEmptyElementsMap[node.nodeName] && !/^(TD|TH)$/.test(node.nodeName)) {
            if (start) {
              rng.setStartBefore(node);
            } else {
              if (node.nodeName == 'BR') {
                rng.setEndBefore(node);
              } else {
                rng.setEndAfter(node);
              }
            }

            return;
          }

          // Found empty text block old IE can place the selection inside those
          if (Env.ie && Env.ie < 11 && this.dom.isBlock(node) && this.dom.isEmpty(node)) {
            if (start) {
              rng.setStart(node, 0);
            } else {
              rng.setEnd(node, 0);
            }

            return;
          }
        } while ((node = (start ? walker.next() : walker.prev())));

        // Failed to find any text node or other suitable location then move to the root of body
        if (root.nodeName == 'BODY') {
          if (start) {
            rng.setStart(root, 0);
          } else {
            rng.setEnd(root, root.childNodes.length);
          }
        }
      },

      getBoundingClientRect: function () {
        var rng = this.getRng();
        return rng.collapsed ? CaretPosition.fromRangeStart(rng).getClientRects()[0] : rng.getBoundingClientRect();
      },

      destroy: function () {
        this.win = null;
        this.controlSelection.destroy();
      }
    };

    return Selection;
  }
);

/**
 * Node.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
 */

/**
 * This class is a minimalistic implementation of a DOM like node used by the DomParser class.
 *
 * @example
 * var node = new tinymce.html.Node('strong', 1);
 * someRoot.append(node);
 *
 * @class tinymce.html.Node
 * @version 3.4
 */
define(
  'tinymce.core.html.Node',
  [
  ],
  function () {
    var whiteSpaceRegExp = /^[ \t\r\n]*$/;
    var typeLookup = {
      '#text': 3,
      '#comment': 8,
      '#cdata': 4,
      '#pi': 7,
      '#doctype': 10,
      '#document-fragment': 11
    };

    // Walks the tree left/right
    var walk = function (node, rootNode, prev) {
      var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next';

      // Walk into nodes if it has a start
      if (node[startName]) {
        return node[startName];
      }

      // Return the sibling if it has one
      if (node !== rootNode) {
        sibling = node[siblingName];

        if (sibling) {
          return sibling;
        }

        // Walk up the parents to look for siblings
        for (parent = node.parent; parent && parent !== rootNode; parent = parent.parent) {
          sibling = parent[siblingName];

          if (sibling) {
            return sibling;
          }
        }
      }
    };

    /**
     * Constructs a new Node instance.
     *
     * @constructor
     * @method Node
     * @param {String} name Name of the node type.
     * @param {Number} type Numeric type representing the node.
     */
    var Node = function (name, type) {
      this.name = name;
      this.type = type;

      if (type === 1) {
        this.attributes = [];
        this.attributes.map = {};
      }
    };

    Node.prototype = {
      /**
       * Replaces the current node with the specified one.
       *
       * @example
       * someNode.replace(someNewNode);
       *
       * @method replace
       * @param {tinymce.html.Node} node Node to replace the current node with.
       * @return {tinymce.html.Node} The old node that got replaced.
       */
      replace: function (node) {
        var self = this;

        if (node.parent) {
          node.remove();
        }

        self.insert(node, self);
        self.remove();

        return self;
      },

      /**
       * Gets/sets or removes an attribute by name.
       *
       * @example
       * someNode.attr("name", "value"); // Sets an attribute
       * console.log(someNode.attr("name")); // Gets an attribute
       * someNode.attr("name", null); // Removes an attribute
       *
       * @method attr
       * @param {String} name Attribute name to set or get.
       * @param {String} value Optional value to set.
       * @return {String/tinymce.html.Node} String or undefined on a get operation or the current node on a set operation.
       */
      attr: function (name, value) {
        var self = this, attrs, i, undef;

        if (typeof name !== "string") {
          for (i in name) {
            self.attr(i, name[i]);
          }

          return self;
        }

        if ((attrs = self.attributes)) {
          if (value !== undef) {
            // Remove attribute
            if (value === null) {
              if (name in attrs.map) {
                delete attrs.map[name];

                i = attrs.length;
                while (i--) {
                  if (attrs[i].name === name) {
                    attrs = attrs.splice(i, 1);
                    return self;
                  }
                }
              }

              return self;
            }

            // Set attribute
            if (name in attrs.map) {
              // Set attribute
              i = attrs.length;
              while (i--) {
                if (attrs[i].name === name) {
                  attrs[i].value = value;
                  break;
                }
              }
            } else {
              attrs.push({ name: name, value: value });
            }

            attrs.map[name] = value;

            return self;
          }

          return attrs.map[name];
        }
      },

      /**
       * Does a shallow clones the node into a new node. It will also exclude id attributes since
       * there should only be one id per document.
       *
       * @example
       * var clonedNode = node.clone();
       *
       * @method clone
       * @return {tinymce.html.Node} New copy of the original node.
       */
      clone: function () {
        var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs;

        // Clone element attributes
        if ((selfAttrs = self.attributes)) {
          cloneAttrs = [];
          cloneAttrs.map = {};

          for (i = 0, l = selfAttrs.length; i < l; i++) {
            selfAttr = selfAttrs[i];

            // Clone everything except id
            if (selfAttr.name !== 'id') {
              cloneAttrs[cloneAttrs.length] = { name: selfAttr.name, value: selfAttr.value };
              cloneAttrs.map[selfAttr.name] = selfAttr.value;
            }
          }

          clone.attributes = cloneAttrs;
        }

        clone.value = self.value;
        clone.shortEnded = self.shortEnded;

        return clone;
      },

      /**
       * Wraps the node in in another node.
       *
       * @example
       * node.wrap(wrapperNode);
       *
       * @method wrap
       */
      wrap: function (wrapper) {
        var self = this;

        self.parent.insert(wrapper, self);
        wrapper.append(self);

        return self;
      },

      /**
       * Unwraps the node in other words it removes the node but keeps the children.
       *
       * @example
       * node.unwrap();
       *
       * @method unwrap
       */
      unwrap: function () {
        var self = this, node, next;

        for (node = self.firstChild; node;) {
          next = node.next;
          self.insert(node, self, true);
          node = next;
        }

        self.remove();
      },

      /**
       * Removes the node from it's parent.
       *
       * @example
       * node.remove();
       *
       * @method remove
       * @return {tinymce.html.Node} Current node that got removed.
       */
      remove: function () {
        var self = this, parent = self.parent, next = self.next, prev = self.prev;

        if (parent) {
          if (parent.firstChild === self) {
            parent.firstChild = next;

            if (next) {
              next.prev = null;
            }
          } else {
            prev.next = next;
          }

          if (parent.lastChild === self) {
            parent.lastChild = prev;

            if (prev) {
              prev.next = null;
            }
          } else {
            next.prev = prev;
          }

          self.parent = self.next = self.prev = null;
        }

        return self;
      },

      /**
       * Appends a new node as a child of the current node.
       *
       * @example
       * node.append(someNode);
       *
       * @method append
       * @param {tinymce.html.Node} node Node to append as a child of the current one.
       * @return {tinymce.html.Node} The node that got appended.
       */
      append: function (node) {
        var self = this, last;

        if (node.parent) {
          node.remove();
        }

        last = self.lastChild;
        if (last) {
          last.next = node;
          node.prev = last;
          self.lastChild = node;
        } else {
          self.lastChild = self.firstChild = node;
        }

        node.parent = self;

        return node;
      },

      /**
       * Inserts a node at a specific position as a child of the current node.
       *
       * @example
       * parentNode.insert(newChildNode, oldChildNode);
       *
       * @method insert
       * @param {tinymce.html.Node} node Node to insert as a child of the current node.
       * @param {tinymce.html.Node} refNode Reference node to set node before/after.
       * @param {Boolean} before Optional state to insert the node before the reference node.
       * @return {tinymce.html.Node} The node that got inserted.
       */
      insert: function (node, refNode, before) {
        var parent;

        if (node.parent) {
          node.remove();
        }

        parent = refNode.parent || this;

        if (before) {
          if (refNode === parent.firstChild) {
            parent.firstChild = node;
          } else {
            refNode.prev.next = node;
          }

          node.prev = refNode.prev;
          node.next = refNode;
          refNode.prev = node;
        } else {
          if (refNode === parent.lastChild) {
            parent.lastChild = node;
          } else {
            refNode.next.prev = node;
          }

          node.next = refNode.next;
          node.prev = refNode;
          refNode.next = node;
        }

        node.parent = parent;

        return node;
      },

      /**
       * Get all children by name.
       *
       * @method getAll
       * @param {String} name Name of the child nodes to collect.
       * @return {Array} Array with child nodes matchin the specified name.
       */
      getAll: function (name) {
        var self = this, node, collection = [];

        for (node = self.firstChild; node; node = walk(node, self)) {
          if (node.name === name) {
            collection.push(node);
          }
        }

        return collection;
      },

      /**
       * Removes all children of the current node.
       *
       * @method empty
       * @return {tinymce.html.Node} The current node that got cleared.
       */
      empty: function () {
        var self = this, nodes, i, node;

        // Remove all children
        if (self.firstChild) {
          nodes = [];

          // Collect the children
          for (node = self.firstChild; node; node = walk(node, self)) {
            nodes.push(node);
          }

          // Remove the children
          i = nodes.length;
          while (i--) {
            node = nodes[i];
            node.parent = node.firstChild = node.lastChild = node.next = node.prev = null;
          }
        }

        self.firstChild = self.lastChild = null;

        return self;
      },

      /**
       * Returns true/false if the node is to be considered empty or not.
       *
       * @example
       * node.isEmpty({img: true});
       * @method isEmpty
       * @param {Object} elements Name/value object with elements that are automatically treated as non empty elements.
       * @param {Object} whitespace Name/value object with elements that are automatically treated whitespace preservables.
       * @param {function} predicate Optional predicate that gets called after the other rules determine that the node is empty. Should return true if the node is a content node.
       * @return {Boolean} true/false if the node is empty or not.
       */
      isEmpty: function (elements, whitespace, predicate) {
        var self = this, node = self.firstChild, i, name;

        whitespace = whitespace || {};

        if (node) {
          do {
            if (node.type === 1) {
              // Ignore bogus elements
              if (node.attributes.map['data-mce-bogus']) {
                continue;
              }

              // Keep empty elements like <img />
              if (elements[node.name]) {
                return false;
              }

              // Keep bookmark nodes and name attribute like <a name="1"></a>
              i = node.attributes.length;
              while (i--) {
                name = node.attributes[i].name;
                if (name === "name" || name.indexOf('data-mce-bookmark') === 0) {
                  return false;
                }
              }
            }

            // Keep comments
            if (node.type === 8) {
              return false;
            }

            // Keep non whitespace text nodes
            if (node.type === 3 && !whiteSpaceRegExp.test(node.value)) {
              return false;
            }

            // Keep whitespace preserve elements
            if (node.type === 3 && node.parent && whitespace[node.parent.name] && whiteSpaceRegExp.test(node.value)) {
              return false;
            }

            // Predicate tells that the node is contents
            if (predicate && predicate(node)) {
              return false;
            }
          } while ((node = walk(node, self)));
        }

        return true;
      },

      /**
       * Walks to the next or previous node and returns that node or null if it wasn't found.
       *
       * @method walk
       * @param {Boolean} prev Optional previous node state defaults to false.
       * @return {tinymce.html.Node} Node that is next to or previous of the current node.
       */
      walk: function (prev) {
        return walk(this, null, prev);
      }
    };

    /**
     * Creates a node of a specific type.
     *
     * @static
     * @method create
     * @param {String} name Name of the node type to create for example "b" or "#text".
     * @param {Object} attrs Name/value collection of attributes that will be applied to elements.
     */
    Node.create = function (name, attrs) {
      var node, attrName;

      // Create node
      node = new Node(name, typeLookup[name] || 1);

      // Add attributes if needed
      if (attrs) {
        for (attrName in attrs) {
          node.attr(attrName, attrs[attrName]);
        }
      }

      return node;
    };

    return Node;
  }
);
/**
 * SaxParser.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
 */

/*eslint max-depth:[2, 9] */

/**
 * This class parses HTML code using pure JavaScript and executes various events for each item it finds. It will
 * always execute the events in the right order for tag soup code like <b><p></b></p>. It will also remove elements
 * and attributes that doesn't fit the schema if the validate setting is enabled.
 *
 * @example
 * var parser = new tinymce.html.SaxParser({
 *     validate: true,
 *
 *     comment: function(text) {
 *         console.log('Comment:', text);
 *     },
 *
 *     cdata: function(text) {
 *         console.log('CDATA:', text);
 *     },
 *
 *     text: function(text, raw) {
 *         console.log('Text:', text, 'Raw:', raw);
 *     },
 *
 *     start: function(name, attrs, empty) {
 *         console.log('Start:', name, attrs, empty);
 *     },
 *
 *     end: function(name) {
 *         console.log('End:', name);
 *     },
 *
 *     pi: function(name, text) {
 *         console.log('PI:', name, text);
 *     },
 *
 *     doctype: function(text) {
 *         console.log('DocType:', text);
 *     }
 * }, schema);
 * @class tinymce.html.SaxParser
 * @version 3.4
 */
define(
  'tinymce.core.html.SaxParser',
  [
    "tinymce.core.html.Schema",
    "tinymce.core.html.Entities",
    "tinymce.core.util.Tools"
  ],
  function (Schema, Entities, Tools) {
    var each = Tools.each;

    var isValidPrefixAttrName = function (name) {
      return name.indexOf('data-') === 0 || name.indexOf('aria-') === 0;
    };

    var trimComments = function (text) {
      return text.replace(/<!--|-->/g, '');
    };

    /**
     * Returns the index of the end tag for a specific start tag. This can be
     * used to skip all children of a parent element from being processed.
     *
     * @private
     * @method findEndTag
     * @param {tinymce.html.Schema} schema Schema instance to use to match short ended elements.
     * @param {String} html HTML string to find the end tag in.
     * @param {Number} startIndex Indext to start searching at should be after the start tag.
     * @return {Number} Index of the end tag.
     */
    var findEndTag = function (schema, html, startIndex) {
      var count = 1, index, matches, tokenRegExp, shortEndedElements;

      shortEndedElements = schema.getShortEndedElements();
      tokenRegExp = /<([!?\/])?([A-Za-z0-9\-_\:\.]+)((?:\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\/|\s+)>/g;
      tokenRegExp.lastIndex = index = startIndex;

      while ((matches = tokenRegExp.exec(html))) {
        index = tokenRegExp.lastIndex;

        if (matches[1] === '/') { // End element
          count--;
        } else if (!matches[1]) { // Start element
          if (matches[2] in shortEndedElements) {
            continue;
          }

          count++;
        }

        if (count === 0) {
          break;
        }
      }

      return index;
    };

    /**
     * Constructs a new SaxParser instance.
     *
     * @constructor
     * @method SaxParser
     * @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks.
     * @param {tinymce.html.Schema} schema HTML Schema class to use when parsing.
     */
    var SaxParser = function (settings, schema) {
      var self = this;

      var noop = function () { };

      settings = settings || {};
      self.schema = schema = schema || new Schema();

      if (settings.fix_self_closing !== false) {
        settings.fix_self_closing = true;
      }

      // Add handler functions from settings and setup default handlers
      each('comment cdata text start end pi doctype'.split(' '), function (name) {
        if (name) {
          self[name] = settings[name] || noop;
        }
      });

      /**
       * Parses the specified HTML string and executes the callbacks for each item it finds.
       *
       * @example
       * new SaxParser({...}).parse('<b>text</b>');
       * @method parse
       * @param {String} html Html string to sax parse.
       */
      self.parse = function (html) {
        var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name;
        var isInternalElement, removeInternalElements, shortEndedElements, fillAttrsMap, isShortEnded;
        var validate, elementRule, isValidElement, attr, attribsValue, validAttributesMap, validAttributePatterns;
        var attributesRequired, attributesDefault, attributesForced, processHtml;
        var anyAttributesRequired, selfClosing, tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0;
        var decode = Entities.decode, fixSelfClosing, filteredUrlAttrs = Tools.makeMap('src,href,data,background,formaction,poster');
        var scriptUriRegExp = /((java|vb)script|mhtml):/i, dataUriRegExp = /^data:/i;

        var processEndTag = function (name) {
          var pos, i;

          // Find position of parent of the same type
          pos = stack.length;
          while (pos--) {
            if (stack[pos].name === name) {
              break;
            }
          }

          // Found parent
          if (pos >= 0) {
            // Close all the open elements
            for (i = stack.length - 1; i >= pos; i--) {
              name = stack[i];

              if (name.valid) {
                self.end(name.name);
              }
            }

            // Remove the open elements from the stack
            stack.length = pos;
          }
        };

        var parseAttribute = function (match, name, value, val2, val3) {
          var attrRule, i, trimRegExp = /[\s\u0000-\u001F]+/g;

          name = name.toLowerCase();
          value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute

          // Validate name and value pass through all data- attributes
          if (validate && !isInternalElement && isValidPrefixAttrName(name) === false) {
            attrRule = validAttributesMap[name];

            // Find rule by pattern matching
            if (!attrRule && validAttributePatterns) {
              i = validAttributePatterns.length;
              while (i--) {
                attrRule = validAttributePatterns[i];
                if (attrRule.pattern.test(name)) {
                  break;
                }
              }

              // No rule matched
              if (i === -1) {
                attrRule = null;
              }
            }

            // No attribute rule found
            if (!attrRule) {
              return;
            }

            // Validate value
            if (attrRule.validValues && !(value in attrRule.validValues)) {
              return;
            }
          }

          // Block any javascript: urls or non image data uris
          if (filteredUrlAttrs[name] && !settings.allow_script_urls) {
            var uri = value.replace(trimRegExp, '');

            try {
              // Might throw malformed URI sequence
              uri = decodeURIComponent(uri);
            } catch (ex) {
              // Fallback to non UTF-8 decoder
              uri = unescape(uri);
            }

            if (scriptUriRegExp.test(uri)) {
              return;
            }

            if (!settings.allow_html_data_urls && dataUriRegExp.test(uri) && !/^data:image\//i.test(uri)) {
              return;
            }
          }

          // Block data or event attributes on elements marked as internal
          if (isInternalElement && (name in filteredUrlAttrs || name.indexOf('on') === 0)) {
            return;
          }

          // Add attribute to list and map
          attrList.map[name] = value;
          attrList.push({
            name: name,
            value: value
          });
        };

        // Precompile RegExps and map objects
        tokenRegExp = new RegExp('<(?:' +
          '(?:!--([\\w\\W]*?)-->)|' + // Comment
          '(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA
          '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE
          '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI
          '(?:\\/([A-Za-z][A-Za-z0-9\\-_\\:\\.]*)>)|' + // End element
          '(?:([A-Za-z][A-Za-z0-9\\-_\\:\\.]*)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element
          ')', 'g');

        attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g;

        // Setup lookup tables for empty elements and boolean attributes
        shortEndedElements = schema.getShortEndedElements();
        selfClosing = settings.self_closing_elements || schema.getSelfClosingElements();
        fillAttrsMap = schema.getBoolAttrs();
        validate = settings.validate;
        removeInternalElements = settings.remove_internals;
        fixSelfClosing = settings.fix_self_closing;
        specialElements = schema.getSpecialElements();
        processHtml = html + '>';

        while ((matches = tokenRegExp.exec(processHtml))) { // Adds and extra '>' to keep regexps from doing catastrofic backtracking on malformed html
          // Text
          if (index < matches.index) {
            self.text(decode(html.substr(index, matches.index - index)));
          }

          if ((value = matches[6])) { // End element
            value = value.toLowerCase();

            // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
            if (value.charAt(0) === ':') {
              value = value.substr(1);
            }

            processEndTag(value);
          } else if ((value = matches[7])) { // Start element
            // Did we consume the extra character then treat it as text
            // This handles the case with html like this: "text a<b text"
            if (matches.index + matches[0].length > html.length) {
              self.text(decode(html.substr(matches.index)));
              index = matches.index + matches[0].length;
              continue;
            }

            value = value.toLowerCase();

            // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
            if (value.charAt(0) === ':') {
              value = value.substr(1);
            }

            isShortEnded = value in shortEndedElements;

            // Is self closing tag for example an <li> after an open <li>
            if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value) {
              processEndTag(value);
            }

            // Validate element
            if (!validate || (elementRule = schema.getElementRule(value))) {
              isValidElement = true;

              // Grab attributes map and patters when validation is enabled
              if (validate) {
                validAttributesMap = elementRule.attributes;
                validAttributePatterns = elementRule.attributePatterns;
              }

              // Parse attributes
              if ((attribsValue = matches[8])) {
                isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element

                // If the element has internal attributes then remove it if we are told to do so
                if (isInternalElement && removeInternalElements) {
                  isValidElement = false;
                }

                attrList = [];
                attrList.map = {};

                attribsValue.replace(attrRegExp, parseAttribute);
              } else {
                attrList = [];
                attrList.map = {};
              }

              // Process attributes if validation is enabled
              if (validate && !isInternalElement) {
                attributesRequired = elementRule.attributesRequired;
                attributesDefault = elementRule.attributesDefault;
                attributesForced = elementRule.attributesForced;
                anyAttributesRequired = elementRule.removeEmptyAttrs;

                // Check if any attribute exists
                if (anyAttributesRequired && !attrList.length) {
                  isValidElement = false;
                }

                // Handle forced attributes
                if (attributesForced) {
                  i = attributesForced.length;
                  while (i--) {
                    attr = attributesForced[i];
                    name = attr.name;
                    attrValue = attr.value;

                    if (attrValue === '{$uid}') {
                      attrValue = 'mce_' + idCount++;
                    }

                    attrList.map[name] = attrValue;
                    attrList.push({ name: name, value: attrValue });
                  }
                }

                // Handle default attributes
                if (attributesDefault) {
                  i = attributesDefault.length;
                  while (i--) {
                    attr = attributesDefault[i];
                    name = attr.name;

                    if (!(name in attrList.map)) {
                      attrValue = attr.value;

                      if (attrValue === '{$uid}') {
                        attrValue = 'mce_' + idCount++;
                      }

                      attrList.map[name] = attrValue;
                      attrList.push({ name: name, value: attrValue });
                    }
                  }
                }

                // Handle required attributes
                if (attributesRequired) {
                  i = attributesRequired.length;
                  while (i--) {
                    if (attributesRequired[i] in attrList.map) {
                      break;
                    }
                  }

                  // None of the required attributes where found
                  if (i === -1) {
                    isValidElement = false;
                  }
                }

                // Invalidate element if it's marked as bogus
                if ((attr = attrList.map['data-mce-bogus'])) {
                  if (attr === 'all') {
                    index = findEndTag(schema, html, tokenRegExp.lastIndex);
                    tokenRegExp.lastIndex = index;
                    continue;
                  }

                  isValidElement = false;
                }
              }

              if (isValidElement) {
                self.start(value, attrList, isShortEnded);
              }
            } else {
              isValidElement = false;
            }

            // Treat script, noscript and style a bit different since they may include code that looks like elements
            if ((endRegExp = specialElements[value])) {
              endRegExp.lastIndex = index = matches.index + matches[0].length;

              if ((matches = endRegExp.exec(html))) {
                if (isValidElement) {
                  text = html.substr(index, matches.index - index);
                }

                index = matches.index + matches[0].length;
              } else {
                text = html.substr(index);
                index = html.length;
              }

              if (isValidElement) {
                if (text.length > 0) {
                  self.text(text, true);
                }

                self.end(value);
              }

              tokenRegExp.lastIndex = index;
              continue;
            }

            // Push value on to stack
            if (!isShortEnded) {
              if (!attribsValue || attribsValue.indexOf('/') != attribsValue.length - 1) {
                stack.push({ name: value, valid: isValidElement });
              } else if (isValidElement) {
                self.end(value);
              }
            }
          } else if ((value = matches[1])) { // Comment
            // Padd comment value to avoid browsers from parsing invalid comments as HTML
            if (value.charAt(0) === '>') {
              value = ' ' + value;
            }

            if (!settings.allow_conditional_comments && value.substr(0, 3).toLowerCase() === '[if') {
              value = ' ' + value;
            }

            self.comment(value);
          } else if ((value = matches[2])) { // CDATA
            self.cdata(trimComments(value));
          } else if ((value = matches[3])) { // DOCTYPE
            self.doctype(value);
          } else if ((value = matches[4])) { // PI
            self.pi(value, matches[5]);
          }

          index = matches.index + matches[0].length;
        }

        // Text
        if (index < html.length) {
          self.text(decode(html.substr(index)));
        }

        // Close any open elements
        for (i = stack.length - 1; i >= 0; i--) {
          value = stack[i];

          if (value.valid) {
            self.end(value.name);
          }
        }
      };
    };

    SaxParser.findEndTag = findEndTag;

    return SaxParser;
  }
);
/**
 * DomParser.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
 */

/**
 * This class parses HTML code into a DOM like structure of nodes it will remove redundant whitespace and make
 * sure that the node tree is valid according to the specified schema.
 * So for example: <p>a<p>b</p>c</p> will become <p>a</p><p>b</p><p>c</p>
 *
 * @example
 * var parser = new tinymce.html.DomParser({validate: true}, schema);
 * var rootNode = parser.parse('<h1>content</h1>');
 *
 * @class tinymce.html.DomParser
 * @version 3.4
 */
define(
  'tinymce.core.html.DomParser',
  [
    "tinymce.core.html.Node",
    "tinymce.core.html.Schema",
    "tinymce.core.html.SaxParser",
    "tinymce.core.util.Tools"
  ],
  function (Node, Schema, SaxParser, Tools) {
    var makeMap = Tools.makeMap, each = Tools.each, explode = Tools.explode, extend = Tools.extend;

    var paddEmptyNode = function (settings, node) {
      if (settings.padd_empty_with_br) {
        node.empty().append(new Node('br', '1')).shortEnded = true;
      } else {
        node.empty().append(new Node('#text', '3')).value = '\u00a0';
      }
    };

    var hasOnlyChild = function (node, name) {
      return node && node.firstChild === node.lastChild && node.firstChild.name === name;
    };

    var isPadded = function (schema, node) {
      var rule = schema.getElementRule(node.name);
      return rule && rule.paddEmpty;
    };

    var isEmpty = function (schema, nonEmptyElements, whitespaceElements, node) {
      return node.isEmpty(nonEmptyElements, whitespaceElements, function (node) {
        return isPadded(schema, node);
      });
    };

    /**
     * Constructs a new DomParser instance.
     *
     * @constructor
     * @method DomParser
     * @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks.
     * @param {tinymce.html.Schema} schema HTML Schema class to use when parsing.
     */
    return function (settings, schema) {
      var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {};

      settings = settings || {};
      settings.validate = "validate" in settings ? settings.validate : true;
      settings.root_name = settings.root_name || 'body';
      self.schema = schema = schema || new Schema();

      var fixInvalidChildren = function (nodes) {
        var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i;
        var nonEmptyElements, whitespaceElements, nonSplitableElements, textBlockElements, specialElements, sibling, nextNode;

        nonSplitableElements = makeMap('tr,td,th,tbody,thead,tfoot,table');
        nonEmptyElements = schema.getNonEmptyElements();
        whitespaceElements = schema.getWhiteSpaceElements();
        textBlockElements = schema.getTextBlockElements();
        specialElements = schema.getSpecialElements();

        for (ni = 0; ni < nodes.length; ni++) {
          node = nodes[ni];

          // Already removed or fixed
          if (!node.parent || node.fixed) {
            continue;
          }

          // If the invalid element is a text block and the text block is within a parent LI element
          // Then unwrap the first text block and convert other sibling text blocks to LI elements similar to Word/Open Office
          if (textBlockElements[node.name] && node.parent.name == 'li') {
            // Move sibling text blocks after LI element
            sibling = node.next;
            while (sibling) {
              if (textBlockElements[sibling.name]) {
                sibling.name = 'li';
                sibling.fixed = true;
                node.parent.insert(sibling, node.parent);
              } else {
                break;
              }

              sibling = sibling.next;
            }

            // Unwrap current text block
            node.unwrap(node);
            continue;
          }

          // Get list of all parent nodes until we find a valid parent to stick the child into
          parents = [node];
          for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) &&
            !nonSplitableElements[parent.name]; parent = parent.parent) {
            parents.push(parent);
          }

          // Found a suitable parent
          if (parent && parents.length > 1) {
            // Reverse the array since it makes looping easier
            parents.reverse();

            // Clone the related parent and insert that after the moved node
            newParent = currentNode = self.filterNode(parents[0].clone());

            // Start cloning and moving children on the left side of the target node
            for (i = 0; i < parents.length - 1; i++) {
              if (schema.isValidChild(currentNode.name, parents[i].name)) {
                tempNode = self.filterNode(parents[i].clone());
                currentNode.append(tempNode);
              } else {
                tempNode = currentNode;
              }

              for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1];) {
                nextNode = childNode.next;
                tempNode.append(childNode);
                childNode = nextNode;
              }

              currentNode = tempNode;
            }

            if (!isEmpty(schema, nonEmptyElements, whitespaceElements, newParent)) {
              parent.insert(newParent, parents[0], true);
              parent.insert(node, newParent);
            } else {
              parent.insert(node, parents[0], true);
            }

            // Check if the element is empty by looking through it's contents and special treatment for <p><br /></p>
            parent = parents[0];
            if (isEmpty(schema, nonEmptyElements, whitespaceElements, parent) || hasOnlyChild(parent, 'br')) {
              parent.empty().remove();
            }
          } else if (node.parent) {
            // If it's an LI try to find a UL/OL for it or wrap it
            if (node.name === 'li') {
              sibling = node.prev;
              if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
                sibling.append(node);
                continue;
              }

              sibling = node.next;
              if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
                sibling.insert(node, sibling.firstChild, true);
                continue;
              }

              node.wrap(self.filterNode(new Node('ul', 1)));
              continue;
            }

            // Try wrapping the element in a DIV
            if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) {
              node.wrap(self.filterNode(new Node('div', 1)));
            } else {
              // We failed wrapping it, then remove or unwrap it
              if (specialElements[node.name]) {
                node.empty().remove();
              } else {
                node.unwrap();
              }
            }
          }
        }
      };

      /**
       * Runs the specified node though the element and attributes filters.
       *
       * @method filterNode
       * @param {tinymce.html.Node} Node the node to run filters on.
       * @return {tinymce.html.Node} The passed in node.
       */
      self.filterNode = function (node) {
        var i, name, list;

        // Run element filters
        if (name in nodeFilters) {
          list = matchedNodes[name];

          if (list) {
            list.push(node);
          } else {
            matchedNodes[name] = [node];
          }
        }

        // Run attribute filters
        i = attributeFilters.length;
        while (i--) {
          name = attributeFilters[i].name;

          if (name in node.attributes.map) {
            list = matchedAttributes[name];

            if (list) {
              list.push(node);
            } else {
              matchedAttributes[name] = [node];
            }
          }
        }

        return node;
      };

      /**
       * Adds a node filter function to the parser, the parser will collect the specified nodes by name
       * and then execute the callback ones it has finished parsing the document.
       *
       * @example
       * parser.addNodeFilter('p,h1', function(nodes, name) {
       *  for (var i = 0; i < nodes.length; i++) {
       *   console.log(nodes[i].name);
       *  }
       * });
       * @method addNodeFilter
       * @method {String} name Comma separated list of nodes to collect.
       * @param {function} callback Callback function to execute once it has collected nodes.
       */
      self.addNodeFilter = function (name, callback) {
        each(explode(name), function (name) {
          var list = nodeFilters[name];

          if (!list) {
            nodeFilters[name] = list = [];
          }

          list.push(callback);
        });
      };

      /**
       * Adds a attribute filter function to the parser, the parser will collect nodes that has the specified attributes
       * and then execute the callback ones it has finished parsing the document.
       *
       * @example
       * parser.addAttributeFilter('src,href', function(nodes, name) {
       *  for (var i = 0; i < nodes.length; i++) {
       *   console.log(nodes[i].name);
       *  }
       * });
       * @method addAttributeFilter
       * @method {String} name Comma separated list of nodes to collect.
       * @param {function} callback Callback function to execute once it has collected nodes.
       */
      self.addAttributeFilter = function (name, callback) {
        each(explode(name), function (name) {
          var i;

          for (i = 0; i < attributeFilters.length; i++) {
            if (attributeFilters[i].name === name) {
              attributeFilters[i].callbacks.push(callback);
              return;
            }
          }

          attributeFilters.push({ name: name, callbacks: [callback] });
        });
      };

      /**
       * Parses the specified HTML string into a DOM like node tree and returns the result.
       *
       * @example
       * var rootNode = new DomParser({...}).parse('<b>text</b>');
       * @method parse
       * @param {String} html Html string to sax parse.
       * @param {Object} args Optional args object that gets passed to all filter functions.
       * @return {tinymce.html.Node} Root node containing the tree.
       */
      self.parse = function (html, args) {
        var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate;
        var blockElements, startWhiteSpaceRegExp, invalidChildren = [], isInWhiteSpacePreservedElement;
        var endWhiteSpaceRegExp, allWhiteSpaceRegExp, isAllWhiteSpaceRegExp, whiteSpaceElements;
        var children, nonEmptyElements, rootBlockName;

        args = args || {};
        matchedNodes = {};
        matchedAttributes = {};
        blockElements = extend(makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements());
        nonEmptyElements = schema.getNonEmptyElements();
        children = schema.children;
        validate = settings.validate;
        rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block;

        whiteSpaceElements = schema.getWhiteSpaceElements();
        startWhiteSpaceRegExp = /^[ \t\r\n]+/;
        endWhiteSpaceRegExp = /[ \t\r\n]+$/;
        allWhiteSpaceRegExp = /[ \t\r\n]+/g;
        isAllWhiteSpaceRegExp = /^[ \t\r\n]+$/;

        var addRootBlocks = function () {
          var node = rootNode.firstChild, next, rootBlockNode;

          // Removes whitespace at beginning and end of block so:
          // <p> x </p> -> <p>x</p>
          var trim = function (rootBlockNode) {
            if (rootBlockNode) {
              node = rootBlockNode.firstChild;
              if (node && node.type == 3) {
                node.value = node.value.replace(startWhiteSpaceRegExp, '');
              }

              node = rootBlockNode.lastChild;
              if (node && node.type == 3) {
                node.value = node.value.replace(endWhiteSpaceRegExp, '');
              }
            }
          };

          // Check if rootBlock is valid within rootNode for example if P is valid in H1 if H1 is the contentEditabe root
          if (!schema.isValidChild(rootNode.name, rootBlockName.toLowerCase())) {
            return;
          }

          while (node) {
            next = node.next;

            if (node.type == 3 || (node.type == 1 && node.name !== 'p' &&
              !blockElements[node.name] && !node.attr('data-mce-type'))) {
              if (!rootBlockNode) {
                // Create a new root block element
                rootBlockNode = createNode(rootBlockName, 1);
                rootBlockNode.attr(settings.forced_root_block_attrs);
                rootNode.insert(rootBlockNode, node);
                rootBlockNode.append(node);
              } else {
                rootBlockNode.append(node);
              }
            } else {
              trim(rootBlockNode);
              rootBlockNode = null;
            }

            node = next;
          }

          trim(rootBlockNode);
        };

        var createNode = function (name, type) {
          var node = new Node(name, type), list;

          if (name in nodeFilters) {
            list = matchedNodes[name];

            if (list) {
              list.push(node);
            } else {
              matchedNodes[name] = [node];
            }
          }

          return node;
        };

        var removeWhitespaceBefore = function (node) {
          var textNode, textNodeNext, textVal, sibling, blockElements = schema.getBlockElements();

          for (textNode = node.prev; textNode && textNode.type === 3;) {
            textVal = textNode.value.replace(endWhiteSpaceRegExp, '');

            // Found a text node with non whitespace then trim that and break
            if (textVal.length > 0) {
              textNode.value = textVal;
              return;
            }

            textNodeNext = textNode.next;

            // Fix for bug #7543 where bogus nodes would produce empty
            // text nodes and these would be removed if a nested list was before it
            if (textNodeNext) {
              if (textNodeNext.type == 3 && textNodeNext.value.length) {
                textNode = textNode.prev;
                continue;
              }

              if (!blockElements[textNodeNext.name] && textNodeNext.name != 'script' && textNodeNext.name != 'style') {
                textNode = textNode.prev;
                continue;
              }
            }

            sibling = textNode.prev;
            textNode.remove();
            textNode = sibling;
          }
        };

        var cloneAndExcludeBlocks = function (input) {
          var name, output = {};

          for (name in input) {
            if (name !== 'li' && name != 'p') {
              output[name] = input[name];
            }
          }

          return output;
        };

        parser = new SaxParser({
          validate: validate,
          allow_script_urls: settings.allow_script_urls,
          allow_conditional_comments: settings.allow_conditional_comments,

          // Exclude P and LI from DOM parsing since it's treated better by the DOM parser
          self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()),

          cdata: function (text) {
            node.append(createNode('#cdata', 4)).value = text;
          },

          text: function (text, raw) {
            var textNode;

            // Trim all redundant whitespace on non white space elements
            if (!isInWhiteSpacePreservedElement) {
              text = text.replace(allWhiteSpaceRegExp, ' ');

              if (node.lastChild && blockElements[node.lastChild.name]) {
                text = text.replace(startWhiteSpaceRegExp, '');
              }
            }

            // Do we need to create the node
            if (text.length !== 0) {
              textNode = createNode('#text', 3);
              textNode.raw = !!raw;
              node.append(textNode).value = text;
            }
          },

          comment: function (text) {
            node.append(createNode('#comment', 8)).value = text;
          },

          pi: function (name, text) {
            node.append(createNode(name, 7)).value = text;
            removeWhitespaceBefore(node);
          },

          doctype: function (text) {
            var newNode;

            newNode = node.append(createNode('#doctype', 10));
            newNode.value = text;
            removeWhitespaceBefore(node);
          },

          start: function (name, attrs, empty) {
            var newNode, attrFiltersLen, elementRule, attrName, parent;

            elementRule = validate ? schema.getElementRule(name) : {};
            if (elementRule) {
              newNode = createNode(elementRule.outputName || name, 1);
              newNode.attributes = attrs;
              newNode.shortEnded = empty;

              node.append(newNode);

              // Check if node is valid child of the parent node is the child is
              // unknown we don't collect it since it's probably a custom element
              parent = children[node.name];
              if (parent && children[newNode.name] && !parent[newNode.name]) {
                invalidChildren.push(newNode);
              }

              attrFiltersLen = attributeFilters.length;
              while (attrFiltersLen--) {
                attrName = attributeFilters[attrFiltersLen].name;

                if (attrName in attrs.map) {
                  list = matchedAttributes[attrName];

                  if (list) {
                    list.push(newNode);
                  } else {
                    matchedAttributes[attrName] = [newNode];
                  }
                }
              }

              // Trim whitespace before block
              if (blockElements[name]) {
                removeWhitespaceBefore(newNode);
              }

              // Change current node if the element wasn't empty i.e not <br /> or <img />
              if (!empty) {
                node = newNode;
              }

              // Check if we are inside a whitespace preserved element
              if (!isInWhiteSpacePreservedElement && whiteSpaceElements[name]) {
                isInWhiteSpacePreservedElement = true;
              }
            }
          },

          end: function (name) {
            var textNode, elementRule, text, sibling, tempNode;

            elementRule = validate ? schema.getElementRule(name) : {};
            if (elementRule) {
              if (blockElements[name]) {
                if (!isInWhiteSpacePreservedElement) {
                  // Trim whitespace of the first node in a block
                  textNode = node.firstChild;
                  if (textNode && textNode.type === 3) {
                    text = textNode.value.replace(startWhiteSpaceRegExp, '');

                    // Any characters left after trim or should we remove it
                    if (text.length > 0) {
                      textNode.value = text;
                      textNode = textNode.next;
                    } else {
                      sibling = textNode.next;
                      textNode.remove();
                      textNode = sibling;

                      // Remove any pure whitespace siblings
                      while (textNode && textNode.type === 3) {
                        text = textNode.value;
                        sibling = textNode.next;

                        if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) {
                          textNode.remove();
                          textNode = sibling;
                        }

                        textNode = sibling;
                      }
                    }
                  }

                  // Trim whitespace of the last node in a block
                  textNode = node.lastChild;
                  if (textNode && textNode.type === 3) {
                    text = textNode.value.replace(endWhiteSpaceRegExp, '');

                    // Any characters left after trim or should we remove it
                    if (text.length > 0) {
                      textNode.value = text;
                      textNode = textNode.prev;
                    } else {
                      sibling = textNode.prev;
                      textNode.remove();
                      textNode = sibling;

                      // Remove any pure whitespace siblings
                      while (textNode && textNode.type === 3) {
                        text = textNode.value;
                        sibling = textNode.prev;

                        if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) {
                          textNode.remove();
                          textNode = sibling;
                        }

                        textNode = sibling;
                      }
                    }
                  }
                }

                // Trim start white space
                // Removed due to: #5424
                /*textNode = node.prev;
                if (textNode && textNode.type === 3) {
                  text = textNode.value.replace(startWhiteSpaceRegExp, '');

                  if (text.length > 0)
                    textNode.value = text;
                  else
                    textNode.remove();
                }*/
              }

              // Check if we exited a whitespace preserved element
              if (isInWhiteSpacePreservedElement && whiteSpaceElements[name]) {
                isInWhiteSpacePreservedElement = false;
              }

              // Handle empty nodes
              if (elementRule.removeEmpty || elementRule.paddEmpty) {
                if (isEmpty(schema, nonEmptyElements, whiteSpaceElements, node)) {
                  if (elementRule.paddEmpty) {
                    paddEmptyNode(settings, node);
                  } else {
                    // Leave nodes that have a name like <a name="name">
                    if (!node.attributes.map.name && !node.attributes.map.id) {
                      tempNode = node.parent;

                      if (blockElements[node.name]) {
                        node.empty().remove();
                      } else {
                        node.unwrap();
                      }

                      node = tempNode;
                      return;
                    }
                  }
                }
              }

              node = node.parent;
            }
          }
        }, schema);

        rootNode = node = new Node(args.context || settings.root_name, 11);

        parser.parse(html);

        // Fix invalid children or report invalid children in a contextual parsing
        if (validate && invalidChildren.length) {
          if (!args.context) {
            fixInvalidChildren(invalidChildren);
          } else {
            args.invalid = true;
          }
        }

        // Wrap nodes in the root into block elements if the root is body
        if (rootBlockName && (rootNode.name == 'body' || args.isRootContent)) {
          addRootBlocks();
        }

        // Run filters only when the contents is valid
        if (!args.invalid) {
          // Run node filters
          for (name in matchedNodes) {
            list = nodeFilters[name];
            nodes = matchedNodes[name];

            // Remove already removed children
            fi = nodes.length;
            while (fi--) {
              if (!nodes[fi].parent) {
                nodes.splice(fi, 1);
              }
            }

            for (i = 0, l = list.length; i < l; i++) {
              list[i](nodes, name, args);
            }
          }

          // Run attribute filters
          for (i = 0, l = attributeFilters.length; i < l; i++) {
            list = attributeFilters[i];

            if (list.name in matchedAttributes) {
              nodes = matchedAttributes[list.name];

              // Remove already removed children
              fi = nodes.length;
              while (fi--) {
                if (!nodes[fi].parent) {
                  nodes.splice(fi, 1);
                }
              }

              for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) {
                list.callbacks[fi](nodes, list.name, args);
              }
            }
          }
        }

        return rootNode;
      };

      // Remove <br> at end of block elements Gecko and WebKit injects BR elements to
      // make it possible to place the caret inside empty blocks. This logic tries to remove
      // these elements and keep br elements that where intended to be there intact
      if (settings.remove_trailing_brs) {
        self.addNodeFilter('br', function (nodes) {
          var i, l = nodes.length, node, blockElements = extend({}, schema.getBlockElements());
          var nonEmptyElements = schema.getNonEmptyElements(), parent, lastParent, prev, prevName;
          var whiteSpaceElements = schema.getNonEmptyElements();
          var elementRule, textNode;

          // Remove brs from body element as well
          blockElements.body = 1;

          // Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p>
          for (i = 0; i < l; i++) {
            node = nodes[i];
            parent = node.parent;

            if (blockElements[node.parent.name] && node === parent.lastChild) {
              // Loop all nodes to the left of the current node and check for other BR elements
              // excluding bookmarks since they are invisible
              prev = node.prev;
              while (prev) {
                prevName = prev.name;

                // Ignore bookmarks
                if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') {
                  // Found a non BR element
                  if (prevName !== "br") {
                    break;
                  }

                  // Found another br it's a <br><br> structure then don't remove anything
                  if (prevName === 'br') {
                    node = null;
                    break;
                  }
                }

                prev = prev.prev;
              }

              if (node) {
                node.remove();

                // Is the parent to be considered empty after we removed the BR
                if (isEmpty(schema, nonEmptyElements, whiteSpaceElements, parent)) {
                  elementRule = schema.getElementRule(parent.name);

                  // Remove or padd the element depending on schema rule
                  if (elementRule) {
                    if (elementRule.removeEmpty) {
                      parent.remove();
                    } else if (elementRule.paddEmpty) {
                      paddEmptyNode(settings, parent);
                    }
                  }
                }
              }
            } else {
              // Replaces BR elements inside inline elements like <p><b><i><br></i></b></p>
              // so they become <p><b><i>&nbsp;</i></b></p>
              lastParent = node;
              while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) {
                lastParent = parent;

                if (blockElements[parent.name]) {
                  break;
                }

                parent = parent.parent;
              }

              if (lastParent === parent && settings.padd_empty_with_br !== true) {
                textNode = new Node('#text', 3);
                textNode.value = '\u00a0';
                node.replace(textNode);
              }
            }
          }
        });
      }


      self.addAttributeFilter('href', function (nodes) {
        var i = nodes.length, node;

        var appendRel = function (rel) {
          var parts = rel.split(' ').filter(function (p) {
            return p.length > 0;
          });
          return parts.concat(['noopener']).sort().join(' ');
        };

        var addNoOpener = function (rel) {
          var newRel = rel ? Tools.trim(rel) : '';
          if (!/\b(noopener)\b/g.test(newRel)) {
            return appendRel(newRel);
          } else {
            return newRel;
          }
        };

        if (!settings.allow_unsafe_link_target) {
          while (i--) {
            node = nodes[i];
            if (node.name === 'a' && node.attr('target') === '_blank') {
              node.attr('rel', addNoOpener(node.attr('rel')));
            }
          }
        }
      });

      // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included.
      if (!settings.allow_html_in_named_anchor) {
        self.addAttributeFilter('id,name', function (nodes) {
          var i = nodes.length, sibling, prevSibling, parent, node;

          while (i--) {
            node = nodes[i];
            if (node.name === 'a' && node.firstChild && !node.attr('href')) {
              parent = node.parent;

              // Move children after current node
              sibling = node.lastChild;
              do {
                prevSibling = sibling.prev;
                parent.insert(sibling, node);
                sibling = prevSibling;
              } while (sibling);
            }
          }
        });
      }

      if (settings.fix_list_elements) {
        self.addNodeFilter('ul,ol', function (nodes) {
          var i = nodes.length, node, parentNode;

          while (i--) {
            node = nodes[i];
            parentNode = node.parent;

            if (parentNode.name === 'ul' || parentNode.name === 'ol') {
              if (node.prev && node.prev.name === 'li') {
                node.prev.append(node);
              } else {
                var li = new Node('li', 1);
                li.attr('style', 'list-style-type: none');
                node.wrap(li);
              }
            }
          }
        });
      }

      if (settings.validate && schema.getValidClasses()) {
        self.addAttributeFilter('class', function (nodes) {
          var i = nodes.length, node, classList, ci, className, classValue;
          var validClasses = schema.getValidClasses(), validClassesMap, valid;

          while (i--) {
            node = nodes[i];
            classList = node.attr('class').split(' ');
            classValue = '';

            for (ci = 0; ci < classList.length; ci++) {
              className = classList[ci];
              valid = false;

              validClassesMap = validClasses['*'];
              if (validClassesMap && validClassesMap[className]) {
                valid = true;
              }

              validClassesMap = validClasses[node.name];
              if (!valid && validClassesMap && validClassesMap[className]) {
                valid = true;
              }

              if (valid) {
                if (classValue) {
                  classValue += ' ';
                }

                classValue += className;
              }
            }

            if (!classValue.length) {
              classValue = null;
            }

            node.attr('class', classValue);
          }
        });
      }
    };
  }
);
/**
 * Writer.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
 */

/**
 * This class is used to write HTML tags out it can be used with the Serializer or the SaxParser.
 *
 * @class tinymce.html.Writer
 * @example
 * var writer = new tinymce.html.Writer({indent: true});
 * var parser = new tinymce.html.SaxParser(writer).parse('<p><br></p>');
 * console.log(writer.getContent());
 *
 * @class tinymce.html.Writer
 * @version 3.4
 */
define(
  'tinymce.core.html.Writer',
  [
    "tinymce.core.html.Entities",
    "tinymce.core.util.Tools"
  ],
  function (Entities, Tools) {
    var makeMap = Tools.makeMap;

    /**
     * Constructs a new Writer instance.
     *
     * @constructor
     * @method Writer
     * @param {Object} settings Name/value settings object.
     */
    return function (settings) {
      var html = [], indent, indentBefore, indentAfter, encode, htmlOutput;

      settings = settings || {};
      indent = settings.indent;
      indentBefore = makeMap(settings.indent_before || '');
      indentAfter = makeMap(settings.indent_after || '');
      encode = Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities);
      htmlOutput = settings.element_format == "html";

      return {
        /**
         * Writes the a start element such as <p id="a">.
         *
         * @method start
         * @param {String} name Name of the element.
         * @param {Array} attrs Optional attribute array or undefined if it hasn't any.
         * @param {Boolean} empty Optional empty state if the tag should end like <br />.
         */
        start: function (name, attrs, empty) {
          var i, l, attr, value;

          if (indent && indentBefore[name] && html.length > 0) {
            value = html[html.length - 1];

            if (value.length > 0 && value !== '\n') {
              html.push('\n');
            }
          }

          html.push('<', name);

          if (attrs) {
            for (i = 0, l = attrs.length; i < l; i++) {
              attr = attrs[i];
              html.push(' ', attr.name, '="', encode(attr.value, true), '"');
            }
          }

          if (!empty || htmlOutput) {
            html[html.length] = '>';
          } else {
            html[html.length] = ' />';
          }

          if (empty && indent && indentAfter[name] && html.length > 0) {
            value = html[html.length - 1];

            if (value.length > 0 && value !== '\n') {
              html.push('\n');
            }
          }
        },

        /**
         * Writes the a end element such as </p>.
         *
         * @method end
         * @param {String} name Name of the element.
         */
        end: function (name) {
          var value;

          /*if (indent && indentBefore[name] && html.length > 0) {
            value = html[html.length - 1];

            if (value.length > 0 && value !== '\n')
              html.push('\n');
          }*/

          html.push('</', name, '>');

          if (indent && indentAfter[name] && html.length > 0) {
            value = html[html.length - 1];

            if (value.length > 0 && value !== '\n') {
              html.push('\n');
            }
          }
        },

        /**
         * Writes a text node.
         *
         * @method text
         * @param {String} text String to write out.
         * @param {Boolean} raw Optional raw state if true the contents wont get encoded.
         */
        text: function (text, raw) {
          if (text.length > 0) {
            html[html.length] = raw ? text : encode(text);
          }
        },

        /**
         * Writes a cdata node such as <![CDATA[data]]>.
         *
         * @method cdata
         * @param {String} text String to write out inside the cdata.
         */
        cdata: function (text) {
          html.push('<![CDATA[', text, ']]>');
        },

        /**
         * Writes a comment node such as <!-- Comment -->.
         *
         * @method cdata
         * @param {String} text String to write out inside the comment.
         */
        comment: function (text) {
          html.push('<!--', text, '-->');
        },

        /**
         * Writes a PI node such as <?xml attr="value" ?>.
         *
         * @method pi
         * @param {String} name Name of the pi.
         * @param {String} text String to write out inside the pi.
         */
        pi: function (name, text) {
          if (text) {
            html.push('<?', name, ' ', encode(text), '?>');
          } else {
            html.push('<?', name, '?>');
          }

          if (indent) {
            html.push('\n');
          }
        },

        /**
         * Writes a doctype node such as <!DOCTYPE data>.
         *
         * @method doctype
         * @param {String} text String to write out inside the doctype.
         */
        doctype: function (text) {
          html.push('<!DOCTYPE', text, '>', indent ? '\n' : '');
        },

        /**
         * Resets the internal buffer if one wants to reuse the writer.
         *
         * @method reset
         */
        reset: function () {
          html.length = 0;
        },

        /**
         * Returns the contents that got serialized.
         *
         * @method getContent
         * @return {String} HTML contents that got written down.
         */
        getContent: function () {
          return html.join('').replace(/\n$/, '');
        }
      };
    };
  }
);
/**
 * Serializer.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
 */

/**
 * This class is used to serialize down the DOM tree into a string using a Writer instance.
 *
 *
 * @example
 * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('<p>text</p>'));
 * @class tinymce.html.Serializer
 * @version 3.4
 */
define(
  'tinymce.core.html.Serializer',
  [
    "tinymce.core.html.Writer",
    "tinymce.core.html.Schema"
  ],
  function (Writer, Schema) {
    /**
     * Constructs a new Serializer instance.
     *
     * @constructor
     * @method Serializer
     * @param {Object} settings Name/value settings object.
     * @param {tinymce.html.Schema} schema Schema instance to use.
     */
    return function (settings, schema) {
      var self = this, writer = new Writer(settings);

      settings = settings || {};
      settings.validate = "validate" in settings ? settings.validate : true;

      self.schema = schema = schema || new Schema();
      self.writer = writer;

      /**
       * Serializes the specified node into a string.
       *
       * @example
       * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('<p>text</p>'));
       * @method serialize
       * @param {tinymce.html.Node} node Node instance to serialize.
       * @return {String} String with HTML based on DOM tree.
       */
      self.serialize = function (node) {
        var handlers, validate;

        validate = settings.validate;

        handlers = {
          // #text
          3: function (node) {
            writer.text(node.value, node.raw);
          },

          // #comment
          8: function (node) {
            writer.comment(node.value);
          },

          // Processing instruction
          7: function (node) {
            writer.pi(node.name, node.value);
          },

          // Doctype
          10: function (node) {
            writer.doctype(node.value);
          },

          // CDATA
          4: function (node) {
            writer.cdata(node.value);
          },

          // Document fragment
          11: function (node) {
            if ((node = node.firstChild)) {
              do {
                walk(node);
              } while ((node = node.next));
            }
          }
        };

        writer.reset();

        var walk = function (node) {
          var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule;

          if (!handler) {
            name = node.name;
            isEmpty = node.shortEnded;
            attrs = node.attributes;

            // Sort attributes
            if (validate && attrs && attrs.length > 1) {
              sortedAttrs = [];
              sortedAttrs.map = {};

              elementRule = schema.getElementRule(node.name);
              if (elementRule) {
                for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) {
                  attrName = elementRule.attributesOrder[i];

                  if (attrName in attrs.map) {
                    attrValue = attrs.map[attrName];
                    sortedAttrs.map[attrName] = attrValue;
                    sortedAttrs.push({ name: attrName, value: attrValue });
                  }
                }

                for (i = 0, l = attrs.length; i < l; i++) {
                  attrName = attrs[i].name;

                  if (!(attrName in sortedAttrs.map)) {
                    attrValue = attrs.map[attrName];
                    sortedAttrs.map[attrName] = attrValue;
                    sortedAttrs.push({ name: attrName, value: attrValue });
                  }
                }

                attrs = sortedAttrs;
              }
            }

            writer.start(node.name, attrs, isEmpty);

            if (!isEmpty) {
              if ((node = node.firstChild)) {
                do {
                  walk(node);
                } while ((node = node.next));
              }

              writer.end(name);
            }
          } else {
            handler(node);
          }
        };

        // Serialize element and treat all non elements as fragments
        if (node.type == 1 && !settings.inner) {
          walk(node);
        } else {
          handlers[11](node);
        }

        return writer.getContent();
      };
    };
  }
);

/**
 * Serializer.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
 */

/**
 * This class is used to serialize DOM trees into a string. Consult the TinyMCE Wiki API for
 * more details and examples on how to use this class.
 *
 * @class tinymce.dom.Serializer
 */
define(
  'tinymce.core.dom.Serializer',
  [
    'global!document',
    'tinymce.core.dom.DOMUtils',
    'tinymce.core.Env',
    'tinymce.core.html.DomParser',
    'tinymce.core.html.Entities',
    'tinymce.core.html.Node',
    'tinymce.core.html.SaxParser',
    'tinymce.core.html.Schema',
    'tinymce.core.html.Serializer',
    'tinymce.core.text.Zwsp',
    'tinymce.core.util.Tools'
  ],
  function (document, DOMUtils, Env, DomParser, Entities, Node, SaxParser, Schema, Serializer, Zwsp, Tools) {
    var each = Tools.each, trim = Tools.trim;
    var DOM = DOMUtils.DOM;

    /**
     * IE 11 has a fantastic bug where it will produce two trailing BR elements to iframe bodies when
     * the iframe is hidden by display: none on a parent container. The DOM is actually out of sync
     * with innerHTML in this case. It's like IE adds shadow DOM BR elements that appears on innerHTML
     * but not as the lastChild of the body. So this fix simply removes the last two
     * BR elements at the end of the document.
     *
     * Example of what happens: <body>text</body> becomes <body>text<br><br></body>
     */
    var trimTrailingBr = function (rootNode) {
      var brNode1, brNode2;

      var isBr = function (node) {
        return node && node.name === 'br';
      };

      brNode1 = rootNode.lastChild;
      if (isBr(brNode1)) {
        brNode2 = brNode1.prev;

        if (isBr(brNode2)) {
          brNode1.remove();
          brNode2.remove();
        }
      }
    };

    /**
     * Constructs a new DOM serializer class.
     *
     * @constructor
     * @method Serializer
     * @param {Object} settings Serializer settings object.
     * @param {tinymce.Editor} editor Optional editor to bind events to and get schema/dom from.
     */
    return function (settings, editor) {
      var dom, schema, htmlParser, tempAttrs = ["data-mce-selected"];

      if (editor) {
        dom = editor.dom;
        schema = editor.schema;
      }

      var trimHtml = function (html) {
        var trimContentRegExp = new RegExp([
          '<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\\/span>', // Trim bogus spans like caret containers
          '\\s?(' + tempAttrs.join('|') + ')="[^"]+"' // Trim temporaty data-mce prefixed attributes like data-mce-selected
        ].join('|'), 'gi');

        html = Zwsp.trim(html.replace(trimContentRegExp, ''));

        return html;
      };

      var trimContent = function (html) {
        var content = html;
        var bogusAllRegExp = /<(\w+) [^>]*data-mce-bogus="all"[^>]*>/g;
        var endTagIndex, index, matchLength, matches, shortEndedElements, schema = editor.schema;

        content = trimHtml(content);
        shortEndedElements = schema.getShortEndedElements();

        // Remove all bogus elements marked with "all"
        while ((matches = bogusAllRegExp.exec(content))) {
          index = bogusAllRegExp.lastIndex;
          matchLength = matches[0].length;

          if (shortEndedElements[matches[1]]) {
            endTagIndex = index;
          } else {
            endTagIndex = SaxParser.findEndTag(schema, content, index);
          }

          content = content.substring(0, index - matchLength) + content.substring(endTagIndex);
          bogusAllRegExp.lastIndex = index - matchLength;
        }

        return content;
      };

      /**
       * Returns a trimmed version of the editor contents to be used for the undo level. This
       * will remove any data-mce-bogus="all" marked elements since these are used for UI it will also
       * remove the data-mce-selected attributes used for selection of objects and caret containers.
       * It will keep all data-mce-bogus="1" elements since these can be used to place the caret etc and will
       * be removed by the serialization logic when you save.
       *
       * @private
       * @return {String} HTML contents of the editor excluding some internal bogus elements.
       */
      var getTrimmedContent = function () {
        return trimContent(editor.getBody().innerHTML);
      };

      var addTempAttr = function (name) {
        if (Tools.inArray(tempAttrs, name) === -1) {
          htmlParser.addAttributeFilter(name, function (nodes, name) {
            var i = nodes.length;

            while (i--) {
              nodes[i].attr(name, null);
            }
          });

          tempAttrs.push(name);
        }
      };

      // Default DOM and Schema if they are undefined
      dom = dom || DOM;
      schema = schema || new Schema(settings);
      settings.entity_encoding = settings.entity_encoding || 'named';
      settings.remove_trailing_brs = "remove_trailing_brs" in settings ? settings.remove_trailing_brs : true;

      htmlParser = new DomParser(settings, schema);

      // Convert tabindex back to elements when serializing contents
      htmlParser.addAttributeFilter('data-mce-tabindex', function (nodes, name) {
        var i = nodes.length, node;

        while (i--) {
          node = nodes[i];
          node.attr('tabindex', node.attributes.map['data-mce-tabindex']);
          node.attr(name, null);
        }
      });

      // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed
      htmlParser.addAttributeFilter('src,href,style', function (nodes, name) {
        var i = nodes.length, node, value, internalName = 'data-mce-' + name;
        var urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef;

        while (i--) {
          node = nodes[i];

          value = node.attributes.map[internalName];
          if (value !== undef) {
            // Set external name to internal value and remove internal
            node.attr(name, value.length > 0 ? value : null);
            node.attr(internalName, null);
          } else {
            // No internal attribute found then convert the value we have in the DOM
            value = node.attributes.map[name];

            if (name === "style") {
              value = dom.serializeStyle(dom.parseStyle(value), node.name);
            } else if (urlConverter) {
              value = urlConverter.call(urlConverterScope, value, name, node.name);
            }

            node.attr(name, value.length > 0 ? value : null);
          }
        }
      });

      // Remove internal classes mceItem<..> or mceSelected
      htmlParser.addAttributeFilter('class', function (nodes) {
        var i = nodes.length, node, value;

        while (i--) {
          node = nodes[i];
          value = node.attr('class');

          if (value) {
            value = node.attr('class').replace(/(?:^|\s)mce-item-\w+(?!\S)/g, '');
            node.attr('class', value.length > 0 ? value : null);
          }
        }
      });

      // Remove bookmark elements
      htmlParser.addAttributeFilter('data-mce-type', function (nodes, name, args) {
        var i = nodes.length, node;

        while (i--) {
          node = nodes[i];

          if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup) {
            node.remove();
          }
        }
      });

      htmlParser.addNodeFilter('noscript', function (nodes) {
        var i = nodes.length, node;

        while (i--) {
          node = nodes[i].firstChild;

          if (node) {
            node.value = Entities.decode(node.value);
          }
        }
      });

      // Force script into CDATA sections and remove the mce- prefix also add comments around styles
      htmlParser.addNodeFilter('script,style', function (nodes, name) {
        var i = nodes.length, node, value, type;

        var trim = function (value) {
          /*jshint maxlen:255 */
          /*eslint max-len:0 */
          return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n')
            .replace(/^[\r\n]*|[\r\n]*$/g, '')
            .replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi, '')
            .replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, '');
        };

        while (i--) {
          node = nodes[i];
          value = node.firstChild ? node.firstChild.value : '';

          if (name === "script") {
            // Remove mce- prefix from script elements and remove default type since the user specified
            // a script element without type attribute
            type = node.attr('type');
            if (type) {
              node.attr('type', type == 'mce-no/type' ? null : type.replace(/^mce\-/, ''));
            }

            if (value.length > 0) {
              node.firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>';
            }
          } else {
            if (value.length > 0) {
              node.firstChild.value = '<!--\n' + trim(value) + '\n-->';
            }
          }
        }
      });

      // Convert comments to cdata and handle protected comments
      htmlParser.addNodeFilter('#comment', function (nodes) {
        var i = nodes.length, node;

        while (i--) {
          node = nodes[i];

          if (node.value.indexOf('[CDATA[') === 0) {
            node.name = '#cdata';
            node.type = 4;
            node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, '');
          } else if (node.value.indexOf('mce:protected ') === 0) {
            node.name = "#text";
            node.type = 3;
            node.raw = true;
            node.value = unescape(node.value).substr(14);
          }
        }
      });

      htmlParser.addNodeFilter('xml:namespace,input', function (nodes, name) {
        var i = nodes.length, node;

        while (i--) {
          node = nodes[i];
          if (node.type === 7) {
            node.remove();
          } else if (node.type === 1) {
            if (name === "input" && !("type" in node.attributes.map)) {
              node.attr('type', 'text');
            }
          }
        }
      });

      // Remove internal data attributes
      htmlParser.addAttributeFilter(
        'data-mce-src,data-mce-href,data-mce-style,' +
        'data-mce-selected,data-mce-expando,' +
        'data-mce-type,data-mce-resize',

        function (nodes, name) {
          var i = nodes.length;

          while (i--) {
            nodes[i].attr(name, null);
          }
        }
      );

      // Return public methods
      return {
        /**
         * Schema instance that was used to when the Serializer was constructed.
         *
         * @field {tinymce.html.Schema} schema
         */
        schema: schema,

        /**
         * Adds a node filter function to the parser used by the serializer, the parser will collect the specified nodes by name
         * and then execute the callback ones it has finished parsing the document.
         *
         * @example
         * parser.addNodeFilter('p,h1', function(nodes, name) {
         *  for (var i = 0; i < nodes.length; i++) {
         *   console.log(nodes[i].name);
         *  }
         * });
         * @method addNodeFilter
         * @method {String} name Comma separated list of nodes to collect.
         * @param {function} callback Callback function to execute once it has collected nodes.
         */
        addNodeFilter: htmlParser.addNodeFilter,

        /**
         * Adds a attribute filter function to the parser used by the serializer, the parser will
         * collect nodes that has the specified attributes
         * and then execute the callback ones it has finished parsing the document.
         *
         * @example
         * parser.addAttributeFilter('src,href', function(nodes, name) {
         *  for (var i = 0; i < nodes.length; i++) {
         *   console.log(nodes[i].name);
         *  }
         * });
         * @method addAttributeFilter
         * @method {String} name Comma separated list of nodes to collect.
         * @param {function} callback Callback function to execute once it has collected nodes.
         */
        addAttributeFilter: htmlParser.addAttributeFilter,

        /**
         * Serializes the specified browser DOM node into a HTML string.
         *
         * @method serialize
         * @param {DOMNode} node DOM node to serialize.
         * @param {Object} args Arguments option that gets passed to event handlers.
         */
        serialize: function (node, args) {
          var self = this, impl, doc, oldDoc, htmlSerializer, content, rootNode;

          // Explorer won't clone contents of script and style and the
          // selected index of select elements are cleared on a clone operation.
          if (Env.ie && dom.select('script,style,select,map').length > 0) {
            content = node.innerHTML;
            node = node.cloneNode(false);
            dom.setHTML(node, content);
          } else {
            node = node.cloneNode(true);
          }

          // Nodes needs to be attached to something in WebKit/Opera
          // This fix will make DOM ranges and make Sizzle happy!
          impl = document.implementation;
          if (impl.createHTMLDocument) {
            // Create an empty HTML document
            doc = impl.createHTMLDocument("");

            // Add the element or it's children if it's a body element to the new document
            each(node.nodeName == 'BODY' ? node.childNodes : [node], function (node) {
              doc.body.appendChild(doc.importNode(node, true));
            });

            // Grab first child or body element for serialization
            if (node.nodeName != 'BODY') {
              node = doc.body.firstChild;
            } else {
              node = doc.body;
            }

            // set the new document in DOMUtils so createElement etc works
            oldDoc = dom.doc;
            dom.doc = doc;
          }

          args = args || {};
          args.format = args.format || 'html';

          // Don't wrap content if we want selected html
          if (args.selection) {
            args.forced_root_block = '';
          }

          // Pre process
          if (!args.no_events) {
            args.node = node;
            self.onPreProcess(args);
          }

          // Parse HTML
          content = Zwsp.trim(trim(args.getInner ? node.innerHTML : dom.getOuterHTML(node)));
          rootNode = htmlParser.parse(content, args);
          trimTrailingBr(rootNode);

          // Serialize HTML
          htmlSerializer = new Serializer(settings, schema);
          args.content = htmlSerializer.serialize(rootNode);

          // Post process
          if (!args.no_events) {
            self.onPostProcess(args);
          }

          // Restore the old document if it was changed
          if (oldDoc) {
            dom.doc = oldDoc;
          }

          args.node = null;

          return args.content;
        },

        /**
         * Adds valid elements rules to the serializers schema instance this enables you to specify things
         * like what elements should be outputted and what attributes specific elements might have.
         * Consult the Wiki for more details on this format.
         *
         * @method addRules
         * @param {String} rules Valid elements rules string to add to schema.
         */
        addRules: function (rules) {
          schema.addValidElements(rules);
        },

        /**
         * Sets the valid elements rules to the serializers schema instance this enables you to specify things
         * like what elements should be outputted and what attributes specific elements might have.
         * Consult the Wiki for more details on this format.
         *
         * @method setRules
         * @param {String} rules Valid elements rules string.
         */
        setRules: function (rules) {
          schema.setValidElements(rules);
        },

        onPreProcess: function (args) {
          if (editor) {
            editor.fire('PreProcess', args);
          }
        },

        onPostProcess: function (args) {
          if (editor) {
            editor.fire('PostProcess', args);
          }
        },

        /**
         * Adds a temporary internal attribute these attributes will get removed on undo and
         * when getting contents out of the editor.
         *
         * @method addTempAttr
         * @param {String} name string
         */
        addTempAttr: addTempAttr,

        // Internal
        trimHtml: trimHtml,
        getTrimmedContent: getTrimmedContent,
        trimContent: trimContent
      };
    };
  }
);

/**
 * InsertList.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 lists into the editor instance.
 *
 * @class tinymce.InsertList
 * @private
 */
define(
  'tinymce.core.InsertList',
  [
    "tinymce.core.util.Tools",
    "tinymce.core.caret.CaretWalker",
    "tinymce.core.caret.CaretPosition"
  ],
  function (Tools, CaretWalker, CaretPosition) {
    var hasOnlyOneChild = function (node) {
      return node.firstChild && node.firstChild === node.lastChild;
    };

    var isPaddingNode = function (node) {
      return node.name === 'br' || node.value === '\u00a0';
    };

    var isPaddedEmptyBlock = function (schema, node) {
      var blockElements = schema.getBlockElements();
      return blockElements[node.name] && hasOnlyOneChild(node) && isPaddingNode(node.firstChild);
    };

    var isEmptyFragmentElement = function (schema, node) {
      var nonEmptyElements = schema.getNonEmptyElements();
      return node && (node.isEmpty(nonEmptyElements) || isPaddedEmptyBlock(schema, node));
    };

    var isListFragment = function (schema, fragment) {
      var firstChild = fragment.firstChild;
      var lastChild = fragment.lastChild;

      // Skip meta since it's likely <meta><ul>..</ul>
      if (firstChild && firstChild.name === 'meta') {
        firstChild = firstChild.next;
      }

      // Skip mce_marker since it's likely <ul>..</ul><span id="mce_marker"></span>
      if (lastChild && lastChild.attr('id') === 'mce_marker') {
        lastChild = lastChild.prev;
      }

      // Skip last child if it's an empty block
      if (isEmptyFragmentElement(schema, lastChild)) {
        lastChild = lastChild.prev;
      }

      if (!firstChild || firstChild !== lastChild) {
        return false;
      }

      return firstChild.name === 'ul' || firstChild.name === 'ol';
    };

    var cleanupDomFragment = function (domFragment) {
      var firstChild = domFragment.firstChild;
      var lastChild = domFragment.lastChild;

      // TODO: remove the meta tag from paste logic
      if (firstChild && firstChild.nodeName === 'META') {
        firstChild.parentNode.removeChild(firstChild);
      }

      if (lastChild && lastChild.id === 'mce_marker') {
        lastChild.parentNode.removeChild(lastChild);
      }

      return domFragment;
    };

    var toDomFragment = function (dom, serializer, fragment) {
      var html = serializer.serialize(fragment);
      var domFragment = dom.createFragment(html);

      return cleanupDomFragment(domFragment);
    };

    var listItems = function (elm) {
      return Tools.grep(elm.childNodes, function (child) {
        return child.nodeName === 'LI';
      });
    };

    var isEmpty = function (elm) {
      return !elm.firstChild;
    };

    var trimListItems = function (elms) {
      return elms.length > 0 && isEmpty(elms[elms.length - 1]) ? elms.slice(0, -1) : elms;
    };

    var getParentLi = function (dom, node) {
      var parentBlock = dom.getParent(node, dom.isBlock);
      return parentBlock && parentBlock.nodeName === 'LI' ? parentBlock : null;
    };

    var isParentBlockLi = function (dom, node) {
      return !!getParentLi(dom, node);
    };

    var getSplit = function (parentNode, rng) {
      var beforeRng = rng.cloneRange();
      var afterRng = rng.cloneRange();

      beforeRng.setStartBefore(parentNode);
      afterRng.setEndAfter(parentNode);

      return [
        beforeRng.cloneContents(),
        afterRng.cloneContents()
      ];
    };

    var findFirstIn = function (node, rootNode) {
      var caretPos = CaretPosition.before(node);
      var caretWalker = new CaretWalker(rootNode);
      var newCaretPos = caretWalker.next(caretPos);

      return newCaretPos ? newCaretPos.toRange() : null;
    };

    var findLastOf = function (node, rootNode) {
      var caretPos = CaretPosition.after(node);
      var caretWalker = new CaretWalker(rootNode);
      var newCaretPos = caretWalker.prev(caretPos);

      return newCaretPos ? newCaretPos.toRange() : null;
    };

    var insertMiddle = function (target, elms, rootNode, rng) {
      var parts = getSplit(target, rng);
      var parentElm = target.parentNode;

      parentElm.insertBefore(parts[0], target);
      Tools.each(elms, function (li) {
        parentElm.insertBefore(li, target);
      });
      parentElm.insertBefore(parts[1], target);
      parentElm.removeChild(target);

      return findLastOf(elms[elms.length - 1], rootNode);
    };

    var insertBefore = function (target, elms, rootNode) {
      var parentElm = target.parentNode;

      Tools.each(elms, function (elm) {
        parentElm.insertBefore(elm, target);
      });

      return findFirstIn(target, rootNode);
    };

    var insertAfter = function (target, elms, rootNode, dom) {
      dom.insertAfter(elms.reverse(), target);
      return findLastOf(elms[0], rootNode);
    };

    var insertAtCaret = function (serializer, dom, rng, fragment) {
      var domFragment = toDomFragment(dom, serializer, fragment);
      var liTarget = getParentLi(dom, rng.startContainer);
      var liElms = trimListItems(listItems(domFragment.firstChild));
      var BEGINNING = 1, END = 2;
      var rootNode = dom.getRoot();

      var isAt = function (location) {
        var caretPos = CaretPosition.fromRangeStart(rng);
        var caretWalker = new CaretWalker(dom.getRoot());
        var newPos = location === BEGINNING ? caretWalker.prev(caretPos) : caretWalker.next(caretPos);

        return newPos ? getParentLi(dom, newPos.getNode()) !== liTarget : true;
      };

      if (isAt(BEGINNING)) {
        return insertBefore(liTarget, liElms, rootNode);
      } else if (isAt(END)) {
        return insertAfter(liTarget, liElms, rootNode, dom);
      }

      return insertMiddle(liTarget, liElms, rootNode, rng);
    };

    return {
      isListFragment: isListFragment,
      insertAtCaret: insertAtCaret,
      isParentBlockLi: isParentBlockLi,
      trimListItems: trimListItems,
      listItems: listItems
    };
  }
);
/**
 * 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.caret.CaretPosition',
    'tinymce.core.caret.CaretWalker',
    'tinymce.core.dom.ElementUtils',
    'tinymce.core.dom.NodeType',
    'tinymce.core.dom.PaddingBr',
    'tinymce.core.dom.RangeNormalizer',
    'tinymce.core.Env',
    'tinymce.core.html.Serializer',
    'tinymce.core.InsertList',
    'tinymce.core.util.Tools'
  ],
  function (Option, Element, CaretPosition, CaretWalker, ElementUtils, NodeType, PaddingBr, RangeNormalizer, Env, Serializer, InsertList, 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(/^&nbsp;/, ' ');
          } else if (!hasSiblingText('previousSibling')) {
            html = html.replace(/^ /, '&nbsp;');
          }

          if (offset < container.length) {
            html = html.replace(/&nbsp;(<br>|)$/, ' ');
          } else if (!hasSiblingText('nextSibling')) {
            html = html.replace(/(&nbsp;| )(<br>|)$/, '&nbsp;');
          }
        }

        return html;
      };

      // Removes &nbsp; from a [b] c -> a &nbsp;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">&#xFEFF;&#x200B;</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 };
      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
    };
  }
);
/**
 * DeleteUtils.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
 */

define(
  'tinymce.core.delete.DeleteUtils',
  [
    'ephox.katamari.api.Option',
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.search.PredicateFind',
    'tinymce.core.dom.ElementType'
  ],
  function (Option, Compare, Element, PredicateFind, ElementType) {
    var isBeforeRoot = function (rootNode) {
      return function (elm) {
        return Compare.eq(rootNode, Element.fromDom(elm.dom().parentNode));
      };
    };

    var getParentBlock = function (rootNode, elm) {
      return Compare.contains(rootNode, elm) ? PredicateFind.closest(elm, function (element) {
        return ElementType.isTextBlock(element) || ElementType.isListItem(element);
      }, isBeforeRoot(rootNode)) : Option.none();
    };

    var placeCaretInEmptyBody = function (editor) {
      var body = editor.getBody();
      var node = body.firstChild && editor.dom.isBlock(body.firstChild) ? body.firstChild : body;
      editor.selection.setCursorLocation(node, 0);
    };

    var paddEmptyBody = function (editor) {
      if (editor.dom.isEmpty(editor.getBody())) {
        editor.setContent('');
        placeCaretInEmptyBody(editor);
      }
    };

    return {
      getParentBlock: getParentBlock,
      paddEmptyBody: paddEmptyBody
    };
  }
);

define(
  'ephox.sugar.api.search.SelectorExists',

  [
    'ephox.sugar.api.search.SelectorFind'
  ],

  function (SelectorFind) {
    var any = function (selector) {
      return SelectorFind.first(selector).isSome();
    };

    var ancestor = function (scope, selector, isRoot) {
      return SelectorFind.ancestor(scope, selector, isRoot).isSome();
    };

    var sibling = function (scope, selector) {
      return SelectorFind.sibling(scope, selector).isSome();
    };

    var child = function (scope, selector) {
      return SelectorFind.child(scope, selector).isSome();
    };

    var descendant = function (scope, selector) {
      return SelectorFind.descendant(scope, selector).isSome();
    };

    var closest = function (scope, selector, isRoot) {
      return SelectorFind.closest(scope, selector, isRoot).isSome();
    };

    return {
      any: any,
      ancestor: ancestor,
      sibling: sibling,
      child: child,
      descendant: descendant,
      closest: closest
    };
  }
);

/**
 * Empty.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
 */

define(
  'tinymce.core.dom.Empty',
  [
    'ephox.katamari.api.Fun',
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.search.SelectorExists',
    'tinymce.core.caret.CaretCandidate',
    'tinymce.core.dom.NodeType',
    'tinymce.core.dom.TreeWalker'
  ],
  function (Fun, Compare, Element, SelectorExists, CaretCandidate, NodeType, TreeWalker) {
    var hasWhitespacePreserveParent = function (rootNode, node) {
      var rootElement = Element.fromDom(rootNode);
      var startNode = Element.fromDom(node);
      return SelectorExists.ancestor(startNode, 'pre,code', Fun.curry(Compare.eq, rootElement));
    };

    var isWhitespace = function (rootNode, node) {
      return NodeType.isText(node) && /^[ \t\r\n]*$/.test(node.data) && hasWhitespacePreserveParent(rootNode, node) === false;
    };

    var isNamedAnchor = function (node) {
      return NodeType.isElement(node) && node.nodeName === 'A' && node.hasAttribute('name');
    };

    var isContent = function (rootNode, node) {
      return (CaretCandidate.isCaretCandidate(node) && isWhitespace(rootNode, node) === false) || isNamedAnchor(node) || isBookmark(node);
    };

    var isBookmark = NodeType.hasAttribute('data-mce-bookmark');
    var isBogus = NodeType.hasAttribute('data-mce-bogus');
    var isBogusAll = NodeType.hasAttributeValue('data-mce-bogus', 'all');

    var isEmptyNode = function (targetNode) {
      var walker, node, brCount = 0;

      if (isContent(targetNode, targetNode)) {
        return false;
      } else {
        node = targetNode.firstChild;
        if (!node) {
          return true;
        }

        walker = new TreeWalker(node, targetNode);
        do {
          if (isBogusAll(node)) {
            node = walker.next(true);
            continue;
          }

          if (isBogus(node)) {
            node = walker.next();
            continue;
          }

          if (NodeType.isBr(node)) {
            brCount++;
            node = walker.next();
            continue;
          }

          if (isContent(targetNode, node)) {
            return false;
          }

          node = walker.next();
        } while (node);

        return brCount <= 1;
      }
    };

    var isEmpty = function (elm) {
      return isEmptyNode(elm.dom());
    };

    return {
      isEmpty: isEmpty
    };
  }
);

/**
 * BlockBoundary.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
 */

define(
  'tinymce.core.delete.BlockBoundary',
  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'ephox.katamari.api.Options',
    'ephox.katamari.api.Struct',
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.node.Node',
    'ephox.sugar.api.search.PredicateFind',
    'ephox.sugar.api.search.Traverse',
    'tinymce.core.caret.CaretFinder',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.delete.DeleteUtils',
    'tinymce.core.dom.Empty',
    'tinymce.core.dom.NodeType'
  ],
  function (Arr, Fun, Option, Options, Struct, Compare, Element, Node, PredicateFind, Traverse, CaretFinder, CaretPosition, DeleteUtils, Empty, NodeType) {
    var BlockPosition = Struct.immutable('block', 'position');
    var BlockBoundary = Struct.immutable('from', 'to');

    var getBlockPosition = function (rootNode, pos) {
      var rootElm = Element.fromDom(rootNode);
      var containerElm = Element.fromDom(pos.container());
      return DeleteUtils.getParentBlock(rootElm, containerElm).map(function (block) {
        return BlockPosition(block, pos);
      });
    };

    var isDifferentBlocks = function (blockBoundary) {
      return Compare.eq(blockBoundary.from().block(), blockBoundary.to().block()) === false;
    };

    var hasSameParent = function (blockBoundary) {
      return Traverse.parent(blockBoundary.from().block()).bind(function (parent1) {
        return Traverse.parent(blockBoundary.to().block()).filter(function (parent2) {
          return Compare.eq(parent1, parent2);
        });
      }).isSome();
    };

    var isEditable = function (blockBoundary) {
      return NodeType.isContentEditableFalse(blockBoundary.from().block()) === false && NodeType.isContentEditableFalse(blockBoundary.to().block()) === false;
    };

    var skipLastBr = function (rootNode, forward, blockPosition) {
      if (NodeType.isBr(blockPosition.position().getNode()) && Empty.isEmpty(blockPosition.block()) === false) {
        return CaretFinder.positionIn(false, blockPosition.block().dom()).bind(function (lastPositionInBlock) {
          if (lastPositionInBlock.isEqual(blockPosition.position())) {
            return CaretFinder.fromPosition(forward, rootNode, lastPositionInBlock).bind(function (to) {
              return getBlockPosition(rootNode, to);
            });
          } else {
            return Option.some(blockPosition);
          }
        }).getOr(blockPosition);
      } else {
        return blockPosition;
      }
    };

    var readFromRange = function (rootNode, forward, rng) {
      var fromBlockPos = getBlockPosition(rootNode, CaretPosition.fromRangeStart(rng));
      var toBlockPos = fromBlockPos.bind(function (blockPos) {
        return CaretFinder.fromPosition(forward, rootNode, blockPos.position()).bind(function (to) {
          return getBlockPosition(rootNode, to).map(function (blockPos) {
            return skipLastBr(rootNode, forward, blockPos);
          });
        });
      });

      return Options.liftN([fromBlockPos, toBlockPos], BlockBoundary).filter(function (blockBoundary) {
        return isDifferentBlocks(blockBoundary) && hasSameParent(blockBoundary) && isEditable(blockBoundary);
      });
    };

    var read = function (rootNode, forward, rng) {
      return rng.collapsed ? readFromRange(rootNode, forward, rng) : Option.none();
    };

    return {
      read: read
    };
  }
);

/**
 * MergeBlocks.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
 */

define(
  'tinymce.core.delete.MergeBlocks',
  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Option',
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.dom.Insert',
    'ephox.sugar.api.dom.Remove',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.search.Traverse',
    'tinymce.core.caret.CaretFinder',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.dom.ElementType',
    'tinymce.core.dom.Empty',
    'tinymce.core.dom.NodeType',
    'tinymce.core.dom.Parents'
  ],
  function (Arr, Option, Compare, Insert, Remove, Element, Traverse, CaretFinder, CaretPosition, ElementType, Empty, NodeType, Parents) {
    var getChildrenUntilBlockBoundary = function (block) {
      var children = Traverse.children(block);
      return Arr.findIndex(children, ElementType.isBlock).fold(
        function () {
          return children;
        },
        function (index) {
          return children.slice(0, index);
        }
      );
    };

    var extractChildren = function (block) {
      var children = getChildrenUntilBlockBoundary(block);

      Arr.each(children, function (node) {
        Remove.remove(node);
      });

      return children;
    };

    var trimBr = function (first, block) {
      CaretFinder.positionIn(first, block.dom()).each(function (position) {
        var node = position.getNode();
        if (NodeType.isBr(node)) {
          Remove.remove(Element.fromDom(node));
        }
      });
    };

    var removeEmptyRoot = function (rootNode, block) {
      var parents = Parents.parentsAndSelf(block, rootNode);
      return Arr.find(parents.reverse(), Empty.isEmpty).each(Remove.remove);
    };

    var findParentInsertPoint = function (toBlock, block) {
      var parents = Traverse.parents(block, function (elm) {
        return Compare.eq(elm, toBlock);
      });

      return Option.from(parents[parents.length - 2]);
    };

    var getInsertionPoint = function (fromBlock, toBlock) {
      if (Compare.contains(toBlock, fromBlock)) {
        return Traverse.parent(fromBlock).bind(function (parent) {
          return Compare.eq(parent, toBlock) ? Option.some(fromBlock) : findParentInsertPoint(toBlock, fromBlock);
        });
      } else {
        return Option.none();
      }
    };

    var mergeBlockInto = function (rootNode, fromBlock, toBlock) {
      if (Empty.isEmpty(toBlock)) {
        Remove.remove(toBlock);
        return CaretFinder.firstPositionIn(fromBlock.dom());
      } else {
        trimBr(true, fromBlock);
        trimBr(false, toBlock);

        var children = extractChildren(fromBlock);

        return getInsertionPoint(fromBlock, toBlock).fold(
          function () {
            removeEmptyRoot(rootNode, fromBlock);

            var position = CaretFinder.lastPositionIn(toBlock.dom());

            Arr.each(children, function (node) {
              Insert.append(toBlock, node);
            });

            return position;
          },
          function (target) {
            var position = CaretFinder.prevPosition(toBlock.dom(), CaretPosition.before(target.dom()));

            Arr.each(children, function (node) {
              Insert.before(target, node);
            });

            removeEmptyRoot(rootNode, fromBlock);

            return position;
          }
        );
      }
    };

    var mergeBlocks = function (rootNode, forward, block1, block2) {
      return forward ? mergeBlockInto(rootNode, block2, block1) : mergeBlockInto(rootNode, block1, block2);
    };

    return {
      mergeBlocks: mergeBlocks
    };
  }
);

/**
 * BlockBoundaryDelete.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
 */

define(
  'tinymce.core.delete.BlockBoundaryDelete',
  [
    'ephox.sugar.api.node.Element',
    'tinymce.core.delete.BlockBoundary',
    'tinymce.core.delete.MergeBlocks'
  ],
  function (Element, BlockBoundary, MergeBlocks) {
    var backspaceDelete = function (editor, forward) {
      var position, rootNode = Element.fromDom(editor.getBody());

      position = BlockBoundary.read(rootNode.dom(), forward, editor.selection.getRng()).bind(function (blockBoundary) {
        return MergeBlocks.mergeBlocks(rootNode, forward, blockBoundary.from().block(), blockBoundary.to().block());
      });

      position.each(function (pos) {
        editor.selection.setRng(pos.toRange());
      });

      return position.isSome();
    };

    return {
      backspaceDelete: backspaceDelete
    };
  }
);

/**
 * BlockRangeDelete.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
 */

define(
  'tinymce.core.delete.BlockRangeDelete',
  [
    'ephox.katamari.api.Options',
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.node.Element',
    'tinymce.core.caret.CaretFinder',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.delete.DeleteUtils',
    'tinymce.core.delete.MergeBlocks'
  ],
  function (Options, Compare, Element, CaretFinder, CaretPosition, DeleteUtils, MergeBlocks) {
    var deleteRangeMergeBlocks = function (rootNode, selection) {
      var rng = selection.getRng();

      return Options.liftN([
        DeleteUtils.getParentBlock(rootNode, Element.fromDom(rng.startContainer)),
        DeleteUtils.getParentBlock(rootNode, Element.fromDom(rng.endContainer))
      ], function (block1, block2) {
        if (Compare.eq(block1, block2) === false) {
          rng.deleteContents();

          MergeBlocks.mergeBlocks(rootNode, true, block1, block2).each(function (pos) {
            selection.setRng(pos.toRange());
          });

          return true;
        } else {
          return false;
        }
      }).getOr(false);
    };

    var isEverythingSelected = function (rootNode, rng) {
      var noPrevious = CaretFinder.prevPosition(rootNode.dom(), CaretPosition.fromRangeStart(rng)).isNone();
      var noNext = CaretFinder.nextPosition(rootNode.dom(), CaretPosition.fromRangeEnd(rng)).isNone();
      return noPrevious && noNext;
    };

    var emptyEditor = function (editor) {
      editor.setContent('');
      editor.selection.setCursorLocation();
      return true;
    };

    var deleteRange = function (editor) {
      var rootNode = Element.fromDom(editor.getBody());
      var rng = editor.selection.getRng();
      return isEverythingSelected(rootNode, rng) ? emptyEditor(editor) : deleteRangeMergeBlocks(rootNode, editor.selection);
    };

    var backspaceDelete = function (editor, forward) {
      return editor.selection.isCollapsed() ? false : deleteRange(editor, editor.selection.getRng());
    };

    return {
      backspaceDelete: backspaceDelete
    };
  }
);

/**
 * CefDeleteAction.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
 */

define(
  'tinymce.core.delete.CefDeleteAction',
  [
    'ephox.katamari.api.Adt',
    'ephox.katamari.api.Option',
    'ephox.sugar.api.node.Element',
    'tinymce.core.caret.CaretFinder',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.caret.CaretUtils',
    'tinymce.core.delete.DeleteUtils',
    'tinymce.core.dom.Empty',
    'tinymce.core.dom.NodeType'
  ],
  function (Adt, Option, Element, CaretFinder, CaretPosition, CaretUtils, DeleteUtils, Empty, NodeType) {
    var DeleteAction = Adt.generate([
      { remove: [ 'element' ] },
      { moveToElement: [ 'element' ] },
      { moveToPosition: [ 'position' ] }
    ]);

    var isAtContentEditableBlockCaret = function (forward, from) {
      var elm = from.getNode(forward === false);
      var caretLocation = forward ? 'after' : 'before';
      return NodeType.isElement(elm) && elm.getAttribute('data-mce-caret') === caretLocation;
    };

    var deleteEmptyBlockOrMoveToCef = function (rootNode, forward, from, to) {
      var toCefElm = to.getNode(forward === false);
      return DeleteUtils.getParentBlock(Element.fromDom(rootNode), Element.fromDom(from.getNode())).map(function (blockElm) {
        return Empty.isEmpty(blockElm) ? DeleteAction.remove(blockElm.dom()) : DeleteAction.moveToElement(toCefElm);
      }).orThunk(function () {
        return Option.some(DeleteAction.moveToElement(toCefElm));
      });
    };

    var findCefPosition = function (rootNode, forward, from) {
      return CaretFinder.fromPosition(forward, rootNode, from).bind(function (to) {
        if (forward && NodeType.isContentEditableFalse(to.getNode())) {
          return deleteEmptyBlockOrMoveToCef(rootNode, forward, from, to);
        } else if (forward === false && NodeType.isContentEditableFalse(to.getNode(true))) {
          return deleteEmptyBlockOrMoveToCef(rootNode, forward, from, to);
        } else if (forward && CaretUtils.isAfterContentEditableFalse(from)) {
          return Option.some(DeleteAction.moveToPosition(to));
        } else if (forward === false && CaretUtils.isBeforeContentEditableFalse(from)) {
          return Option.some(DeleteAction.moveToPosition(to));
        } else {
          return Option.none();
        }
      });
    };

    var getContentEditableBlockAction = function (forward, elm) {
      if (forward && NodeType.isContentEditableFalse(elm.nextSibling)) {
        return Option.some(DeleteAction.moveToElement(elm.nextSibling));
      } else if (forward === false && NodeType.isContentEditableFalse(elm.previousSibling)) {
        return Option.some(DeleteAction.moveToElement(elm.previousSibling));
      } else {
        return Option.none();
      }
    };

    var getContentEditableAction = function (rootNode, forward, from) {
      if (isAtContentEditableBlockCaret(forward, from)) {
        return getContentEditableBlockAction(forward, from.getNode(forward === false))
          .fold(
            function () {
              return findCefPosition(rootNode, forward, from);
            },
            Option.some
          );
      } else {
        return findCefPosition(rootNode, forward, from);
      }
    };

    var read = function (rootNode, forward, rng) {
      var normalizedRange = CaretUtils.normalizeRange(forward ? 1 : -1, rootNode, rng);
      var from = CaretPosition.fromRangeStart(normalizedRange);

      if (forward === false && CaretUtils.isAfterContentEditableFalse(from)) {
        return Option.some(DeleteAction.remove(from.getNode(true)));
      } else if (forward && CaretUtils.isBeforeContentEditableFalse(from)) {
        return Option.some(DeleteAction.remove(from.getNode()));
      } else {
        return getContentEditableAction(rootNode, forward, from);
      }
    };

    return {
      read: read
    };
  }
);

/**
 * DeleteElement.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
 */

define(
  'tinymce.core.delete.DeleteElement',
  [
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'ephox.katamari.api.Options',
    'ephox.sugar.api.dom.Insert',
    'ephox.sugar.api.dom.Remove',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.node.Node',
    'ephox.sugar.api.search.PredicateFind',
    'ephox.sugar.api.search.Traverse',
    'tinymce.core.caret.CaretCandidate',
    'tinymce.core.caret.CaretFinder',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.dom.Empty',
    'tinymce.core.dom.NodeType'
  ],
  function (Fun, Option, Options, Insert, Remove, Element, Node, PredicateFind, Traverse, CaretCandidate, CaretFinder, CaretPosition, Empty, NodeType) {
    var needsReposition = function (pos, elm) {
      var container = pos.container();
      var offset = pos.offset();
      return CaretPosition.isTextPosition(pos) === false && container === elm.parentNode && offset > CaretPosition.before(elm).offset();
    };

    var reposition = function (elm, pos) {
      return needsReposition(pos, elm) ? new CaretPosition(pos.container(), pos.offset() - 1) : pos;
    };

    var beforeOrStartOf = function (node) {
      return NodeType.isText(node) ? new CaretPosition(node, 0) : CaretPosition.before(node);
    };

    var afterOrEndOf = function (node) {
      return NodeType.isText(node) ? new CaretPosition(node, node.data.length) : CaretPosition.after(node);
    };

    var getPreviousSiblingCaretPosition = function (elm) {
      if (CaretCandidate.isCaretCandidate(elm.previousSibling)) {
        return Option.some(afterOrEndOf(elm.previousSibling));
      } else {
        return elm.previousSibling ? CaretFinder.lastPositionIn(elm.previousSibling) : Option.none();
      }
    };

    var getNextSiblingCaretPosition = function (elm) {
      if (CaretCandidate.isCaretCandidate(elm.nextSibling)) {
        return Option.some(beforeOrStartOf(elm.nextSibling));
      } else {
        return elm.nextSibling ? CaretFinder.firstPositionIn(elm.nextSibling) : Option.none();
      }
    };

    var findCaretPositionBackwardsFromElm = function (rootElement, elm) {
      var startPosition = CaretPosition.before(elm.previousSibling ? elm.previousSibling : elm.parentNode);
      return CaretFinder.prevPosition(rootElement, startPosition).fold(
        function () {
          return CaretFinder.nextPosition(rootElement, CaretPosition.after(elm));
        },
        Option.some
      );
    };

    var findCaretPositionForwardsFromElm = function (rootElement, elm) {
      return CaretFinder.nextPosition(rootElement, CaretPosition.after(elm)).fold(
        function () {
          return CaretFinder.prevPosition(rootElement, CaretPosition.before(elm));
        },
        Option.some
      );
    };

    var findCaretPositionBackwards = function (rootElement, elm) {
      return getPreviousSiblingCaretPosition(elm).orThunk(function () {
        return getNextSiblingCaretPosition(elm);
      }).orThunk(function () {
        return findCaretPositionBackwardsFromElm(rootElement, elm);
      });
    };

    var findCaretPositionForward = function (rootElement, elm) {
      return getNextSiblingCaretPosition(elm).orThunk(function () {
        return getPreviousSiblingCaretPosition(elm);
      }).orThunk(function () {
        return findCaretPositionForwardsFromElm(rootElement, elm);
      });
    };

    var findCaretPosition = function (forward, rootElement, elm) {
      return forward ? findCaretPositionForward(rootElement, elm) : findCaretPositionBackwards(rootElement, elm);
    };

    var findCaretPosOutsideElmAfterDelete = function (forward, rootElement, elm) {
      return findCaretPosition(forward, rootElement, elm).map(Fun.curry(reposition, elm));
    };

    var setSelection = function (editor, forward, pos) {
      pos.fold(
        function () {
          editor.focus();
        },
        function (pos) {
          editor.selection.setRng(pos.toRange(), forward);
        }
      );
    };

    var eqRawNode = function (rawNode) {
      return function (elm) {
        return elm.dom() === rawNode;
      };
    };

    var isBlock = function (editor, elm) {
      return elm && editor.schema.getBlockElements().hasOwnProperty(Node.name(elm));
    };

    var paddEmptyBlock = function (elm) {
      if (Empty.isEmpty(elm)) {
        var br = Element.fromHtml('<br data-mce-bogus="1">');
        Remove.empty(elm);
        Insert.append(elm, br);
        return Option.some(CaretPosition.before(br.dom()));
      } else {
        return Option.none();
      }
    };

    // When deleting an element between two text nodes IE 11 doesn't automatically merge the adjacent text nodes
    var deleteNormalized = function (elm, afterDeletePosOpt) {
      return Options.liftN([Traverse.prevSibling(elm), Traverse.nextSibling(elm), afterDeletePosOpt], function (prev, next, afterDeletePos) {
        var offset, prevNode = prev.dom(), nextNode = next.dom();

        if (NodeType.isText(prevNode) && NodeType.isText(nextNode)) {
          offset = prevNode.data.length;
          prevNode.appendData(nextNode.data);
          Remove.remove(next);
          Remove.remove(elm);
          if (afterDeletePos.container() === nextNode) {
            return new CaretPosition(prevNode, offset);
          } else {
            return afterDeletePos;
          }
        } else {
          Remove.remove(elm);
          return afterDeletePos;
        }
      }).orThunk(function () {
        Remove.remove(elm);
        return afterDeletePosOpt;
      });
    };

    var deleteElement = function (editor, forward, elm) {
      var afterDeletePos = findCaretPosOutsideElmAfterDelete(forward, editor.getBody(), elm.dom());
      var parentBlock = PredicateFind.ancestor(elm, Fun.curry(isBlock, editor), eqRawNode(editor.getBody()));
      var normalizedAfterDeletePos = deleteNormalized(elm, afterDeletePos);

      parentBlock.bind(paddEmptyBlock).fold(
        function () {
          setSelection(editor, forward, normalizedAfterDeletePos);
        },
        function (paddPos) {
          setSelection(editor, forward, Option.some(paddPos));
        }
      );
    };

    return {
      deleteElement: deleteElement
    };
  }
);
/**
 * CefDelete.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
 */

define(
  'tinymce.core.delete.CefDelete',
  [
    'ephox.katamari.api.Arr',
    'ephox.sugar.api.dom.Remove',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.search.SelectorFilter',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.delete.CefDeleteAction',
    'tinymce.core.delete.DeleteElement',
    'tinymce.core.delete.DeleteUtils',
    'tinymce.core.dom.NodeType'
  ],
  function (Arr, Remove, Element, SelectorFilter, CaretPosition, CefDeleteAction, DeleteElement, DeleteUtils, NodeType) {
    var deleteElement = function (editor, forward) {
      return function (element) {
        DeleteElement.deleteElement(editor, forward, Element.fromDom(element));
        return true;
      };
    };

    var moveToElement = function (editor, forward) {
      return function (element) {
        var pos = forward ? CaretPosition.before(element) : CaretPosition.after(element);
        editor.selection.setRng(pos.toRange());
        return true;
      };
    };

    var moveToPosition = function (editor) {
      return function (pos) {
        editor.selection.setRng(pos.toRange());
        return true;
      };
    };

    var backspaceDeleteCaret = function (editor, forward) {
      var result = CefDeleteAction.read(editor.getBody(), forward, editor.selection.getRng()).map(function (deleteAction) {
        return deleteAction.fold(
          deleteElement(editor, forward),
          moveToElement(editor, forward),
          moveToPosition(editor)
        );
      });

      return result.getOr(false);
    };

    var deleteOffscreenSelection = function (rootElement) {
      Arr.each(SelectorFilter.descendants(rootElement, '.mce-offscreen-selection'), Remove.remove);
    };

    var backspaceDeleteRange = function (editor, forward) {
      var selectedElement = editor.selection.getNode();
      if (NodeType.isContentEditableFalse(selectedElement)) {
        deleteOffscreenSelection(Element.fromDom(editor.getBody()));
        DeleteElement.deleteElement(editor, forward, Element.fromDom(editor.selection.getNode()));
        DeleteUtils.paddEmptyBody(editor);
        return true;
      } else {
        return false;
      }
    };

    var getContentEditableRoot = function (root, node) {
      while (node && node !== root) {
        if (NodeType.isContentEditableTrue(node) || NodeType.isContentEditableFalse(node)) {
          return node;
        }

        node = node.parentNode;
      }

      return null;
    };

    var paddEmptyElement = function (editor) {
      var br, ceRoot = getContentEditableRoot(editor.getBody(), editor.selection.getNode());

      if (NodeType.isContentEditableTrue(ceRoot) && editor.dom.isBlock(ceRoot) && editor.dom.isEmpty(ceRoot)) {
        br = editor.dom.create('br', { "data-mce-bogus": "1" });
        editor.dom.setHTML(ceRoot, '');
        ceRoot.appendChild(br);
        editor.selection.setRng(CaretPosition.before(br).toRange());
      }

      return true;
    };

    var backspaceDelete = function (editor, forward) {
      if (editor.selection.isCollapsed()) {
        return backspaceDeleteCaret(editor, forward);
      } else {
        return backspaceDeleteRange(editor, forward);
      }
    };

    return {
      backspaceDelete: backspaceDelete,
      paddEmptyElement: paddEmptyElement
    };
  }
);

/**
 * CaretContainerInline.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
 */

define(
  'tinymce.core.caret.CaretContainerInline',
  [
    'ephox.katamari.api.Fun',
    'tinymce.core.dom.NodeType',
    'tinymce.core.text.Zwsp'
  ],
  function (Fun, NodeType, Zwsp) {
    var isText = NodeType.isText;

    var startsWithCaretContainer = function (node) {
      return isText(node) && node.data[0] === Zwsp.ZWSP;
    };

    var endsWithCaretContainer = function (node) {
      return isText(node) && node.data[node.data.length - 1] === Zwsp.ZWSP;
    };

    var createZwsp = function (node) {
      return node.ownerDocument.createTextNode(Zwsp.ZWSP);
    };

    var insertBefore = function (node) {
      if (isText(node.previousSibling)) {
        if (endsWithCaretContainer(node.previousSibling)) {
          return node.previousSibling;
        } else {
          node.previousSibling.appendData(Zwsp.ZWSP);
          return node.previousSibling;
        }
      } else if (isText(node)) {
        if (startsWithCaretContainer(node)) {
          return node;
        } else {
          node.insertData(0, Zwsp.ZWSP);
          return node;
        }
      } else {
        var newNode = createZwsp(node);
        node.parentNode.insertBefore(newNode, node);
        return newNode;
      }
    };

    var insertAfter = function (node) {
      if (isText(node.nextSibling)) {
        if (startsWithCaretContainer(node.nextSibling)) {
          return node.nextSibling;
        } else {
          node.nextSibling.insertData(0, Zwsp.ZWSP);
          return node.nextSibling;
        }
      } else if (isText(node)) {
        if (endsWithCaretContainer(node)) {
          return node;
        } else {
          node.appendData(Zwsp.ZWSP);
          return node;
        }
      } else {
        var newNode = createZwsp(node);
        if (node.nextSibling) {
          node.parentNode.insertBefore(newNode, node.nextSibling);
        } else {
          node.parentNode.appendChild(newNode);
        }
        return newNode;
      }
    };

    var insertInline = function (before, node) {
      return before ? insertBefore(node) : insertAfter(node);
    };

    return {
      insertInline: insertInline,
      insertInlineBefore: Fun.curry(insertInline, true),
      insertInlineAfter: Fun.curry(insertInline, false)
    };
  }
);
/**
 * CaretContainerRemove.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
 */

define(
  'tinymce.core.caret.CaretContainerRemove',
  [
    'ephox.katamari.api.Arr',
    'tinymce.core.caret.CaretContainer',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.dom.NodeType',
    'tinymce.core.text.Zwsp'
  ],
  function (Arr, CaretContainer, CaretPosition, NodeType, Zwsp) {
    var isElement = NodeType.isElement;
    var isText = NodeType.isText;

    var removeNode = function (node) {
      var parentNode = node.parentNode;
      if (parentNode) {
        parentNode.removeChild(node);
      }
    };

    var getNodeValue = function (node) {
      try {
        return node.nodeValue;
      } catch (ex) {
        // IE sometimes produces "Invalid argument" on nodes
        return "";
      }
    };

    var setNodeValue = function (node, text) {
      if (text.length === 0) {
        removeNode(node);
      } else {
        node.nodeValue = text;
      }
    };

    var trimCount = function (text) {
      var trimmedText = Zwsp.trim(text);
      return { count: text.length - trimmedText.length, text: trimmedText };
    };

    var removeUnchanged = function (caretContainer, pos) {
      remove(caretContainer);
      return pos;
    };

    var removeTextAndReposition = function (caretContainer, pos) {
      var before = trimCount(caretContainer.data.substr(0, pos.offset()));
      var after = trimCount(caretContainer.data.substr(pos.offset()));
      var text = before.text + after.text;

      if (text.length > 0) {
        setNodeValue(caretContainer, text);
        return new CaretPosition(caretContainer, pos.offset() - before.count);
      } else {
        return pos;
      }
    };

    var removeElementAndReposition = function (caretContainer, pos) {
      var parentNode = pos.container();
      var newPosition = Arr.indexOf(parentNode.childNodes, caretContainer).map(function (index) {
        return index < pos.offset() ? new CaretPosition(parentNode, pos.offset() - 1) : pos;
      }).getOr(pos);
      remove(caretContainer);
      return newPosition;
    };

    var removeTextCaretContainer = function (caretContainer, pos) {
      return pos.container() === caretContainer ? removeTextAndReposition(caretContainer, pos) : removeUnchanged(caretContainer, pos);
    };

    var removeElementCaretContainer = function (caretContainer, pos) {
      return pos.container() === caretContainer.parentNode ? removeElementAndReposition(caretContainer, pos) : removeUnchanged(caretContainer, pos);
    };

    var removeAndReposition = function (container, pos) {
      return CaretPosition.isTextPosition(pos) ? removeTextCaretContainer(container, pos) : removeElementCaretContainer(container, pos);
    };

    var remove = function (caretContainerNode) {
      if (isElement(caretContainerNode) && CaretContainer.isCaretContainer(caretContainerNode)) {
        if (CaretContainer.hasContent(caretContainerNode)) {
          caretContainerNode.removeAttribute('data-mce-caret');
        } else {
          removeNode(caretContainerNode);
        }
      }

      if (isText(caretContainerNode)) {
        var text = Zwsp.trim(getNodeValue(caretContainerNode));
        setNodeValue(caretContainerNode, text);
      }
    };

    return {
      removeAndReposition: removeAndReposition,
      remove: remove
    };
  }
);
/**
 * DefaultSettings.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
 */

define(
  'tinymce.core.EditorSettings',
  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Obj',
    'ephox.katamari.api.Option',
    'ephox.katamari.api.Strings',
    'ephox.katamari.api.Struct',
    'ephox.katamari.api.Type',
    'ephox.sand.api.PlatformDetection',
    'tinymce.core.util.Tools'
  ],
  function (Arr, Fun, Obj, Option, Strings, Struct, Type, PlatformDetection, Tools) {
    var sectionResult = Struct.immutable('sections', 'settings');
    var detection = PlatformDetection.detect();
    var isTouch = detection.deviceType.isTouch();
    var mobilePlugins = [ 'lists', 'autolink', 'autosave' ];
    var defaultMobileSettings = { theme: 'mobile' };

    var normalizePlugins = function (plugins) {
      var pluginNames = Type.isArray(plugins) ? plugins.join(' ') : plugins;
      var trimmedPlugins = Arr.map(Type.isString(pluginNames) ? pluginNames.split(' ') : [ ], Strings.trim);
      return Arr.filter(trimmedPlugins, function (item) {
        return item.length > 0;
      });
    };

    var filterMobilePlugins = function (plugins) {
      return Arr.filter(plugins, Fun.curry(Arr.contains, mobilePlugins));
    };

    var extractSections = function (keys, settings) {
      var result = Obj.bifilter(settings, function (value, key) {
        return Arr.contains(keys, key);
      });

      return sectionResult(result.t, result.f);
    };

    var getSection = function (sectionResult, name, defaults) {
      var sections = sectionResult.sections();
      var sectionSettings = sections.hasOwnProperty(name) ? sections[name] : { };
      return Tools.extend({}, defaults, sectionSettings);
    };

    var hasSection = function (sectionResult, name) {
      return sectionResult.sections().hasOwnProperty(name);
    };

    var getDefaultSettings = function (id, documentBaseUrl, editor) {
      return {
        id: id,
        theme: 'modern',
        delta_width: 0,
        delta_height: 0,
        popup_css: '',
        plugins: '',
        document_base_url: documentBaseUrl,
        add_form_submit_trigger: true,
        submit_patch: true,
        add_unload_trigger: true,
        convert_urls: true,
        relative_urls: true,
        remove_script_host: true,
        object_resizing: true,
        doctype: '<!DOCTYPE html>',
        visual: true,
        font_size_style_values: 'xx-small,x-small,small,medium,large,x-large,xx-large',

        // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size
        font_size_legacy_values: 'xx-small,small,medium,large,x-large,xx-large,300%',
        forced_root_block: 'p',
        hidden_input: true,
        padd_empty_editor: true,
        render_ui: true,
        indentation: '30px',
        inline_styles: true,
        convert_fonts_to_spans: true,
        indent: 'simple',
        indent_before: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' +
        'tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist',
        indent_after: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' +
        'tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist',
        entity_encoding: 'named',
        url_converter: editor.convertURL,
        url_converter_scope: editor,
        ie7_compat: true
      };
    };

    var getExternalPlugins = function (overrideSettings, settings) {
      var userDefinedExternalPlugins = settings.external_plugins ? settings.external_plugins : { };

      if (overrideSettings && overrideSettings.external_plugins) {
        return Tools.extend({}, overrideSettings.external_plugins, userDefinedExternalPlugins);
      } else {
        return userDefinedExternalPlugins;
      }
    };

    var combinePlugins = function (forcedPlugins, plugins) {
      return [].concat(normalizePlugins(forcedPlugins)).concat(normalizePlugins(plugins));
    };

    var processPlugins = function (isTouchDevice, sectionResult, defaultOverrideSettings, settings) {
      var forcedPlugins = normalizePlugins(defaultOverrideSettings.forced_plugins);
      var plugins = normalizePlugins(settings.plugins);
      var platformPlugins = isTouchDevice && hasSection(sectionResult, 'mobile') ? filterMobilePlugins(plugins) : plugins;
      var combinedPlugins = combinePlugins(forcedPlugins, platformPlugins);

      return Tools.extend(settings, {
        plugins: combinedPlugins.join(' ')
      });
    };

    var isOnMobile = function (isTouchDevice, sectionResult) {
      var isInline = sectionResult.settings().inline; // We don't support mobile inline yet
      return isTouchDevice && hasSection(sectionResult, 'mobile') && !isInline;
    };

    var combineSettings = function (isTouchDevice, defaultSettings, defaultOverrideSettings, settings) {
      var sectionResult = extractSections(['mobile'], settings);
      var extendedSettings = Tools.extend(
        // Default settings
        defaultSettings,

        // tinymce.overrideDefaults settings
        defaultOverrideSettings,

        // User settings
        sectionResult.settings(),

        // Sections
        isOnMobile(isTouchDevice, sectionResult) ? getSection(sectionResult, 'mobile', defaultMobileSettings) : { },

        // Forced settings
        {
          validate: true,
          content_editable: sectionResult.settings().inline,
          external_plugins: getExternalPlugins(defaultOverrideSettings, sectionResult.settings())
        }
      );

      return processPlugins(isTouchDevice, sectionResult, defaultOverrideSettings, extendedSettings);
    };

    var getEditorSettings = function (editor, id, documentBaseUrl, defaultOverrideSettings, settings) {
      var defaultSettings = getDefaultSettings(id, documentBaseUrl, editor);
      return combineSettings(isTouch, defaultSettings, defaultOverrideSettings, settings);
    };

    var get = function (editor, name) {
      return Option.from(editor.settings[name]);
    };

    var getFiltered = function (predicate, editor, name) {
      return Option.from(editor.settings[name]).filter(predicate);
    };

    return {
      getEditorSettings: getEditorSettings,
      get: get,
      getString: Fun.curry(getFiltered, Type.isString),
      combineSettings: combineSettings
    };
  }
);

/**
 * Bidi.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
 */

define(
  'tinymce.core.text.Bidi',
  [
  ],
  function () {
    var strongRtl = /[\u0591-\u07FF\uFB1D-\uFDFF\uFE70-\uFEFC]/;

    var hasStrongRtl = function (text) {
      return strongRtl.test(text);
    };

    return {
      hasStrongRtl: hasStrongRtl
    };
  }
);
/**
 * InlineUtils.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
 */

define(
  'tinymce.core.keyboard.InlineUtils',
  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'ephox.katamari.api.Options',
    'ephox.katamari.api.Type',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.search.Selectors',
    'tinymce.core.caret.CaretContainer',
    'tinymce.core.caret.CaretFinder',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.caret.CaretUtils',
    'tinymce.core.caret.CaretWalker',
    'tinymce.core.dom.DOMUtils',
    'tinymce.core.dom.NodeType',
    'tinymce.core.EditorSettings',
    'tinymce.core.text.Bidi'
  ],
  function (
    Arr, Fun, Option, Options, Type, Element, Selectors, CaretContainer, CaretFinder, CaretPosition, CaretUtils, CaretWalker, DOMUtils, NodeType, EditorSettings,
    Bidi
  ) {
    var isInlineTarget = function (editor, elm) {
      var selector = EditorSettings.getString(editor, 'inline_boundaries_selector').getOr('a[href],code');
      return Selectors.is(Element.fromDom(elm), selector);
    };

    var isRtl = function (element) {
      return DOMUtils.DOM.getStyle(element, 'direction', true) === 'rtl' || Bidi.hasStrongRtl(element.textContent);
    };

    var findInlineParents = function (isInlineTarget, rootNode, pos) {
      return Arr.filter(DOMUtils.DOM.getParents(pos.container(), '*', rootNode), isInlineTarget);
    };

    var findRootInline = function (isInlineTarget, rootNode, pos) {
      var parents = findInlineParents(isInlineTarget, rootNode, pos);
      return Option.from(parents[parents.length - 1]);
    };

    var hasSameParentBlock = function (rootNode, node1, node2) {
      var block1 = CaretUtils.getParentBlock(node1, rootNode);
      var block2 = CaretUtils.getParentBlock(node2, rootNode);
      return block1 && block1 === block2;
    };

    var isAtZwsp = function (pos) {
      return CaretContainer.isBeforeInline(pos) || CaretContainer.isAfterInline(pos);
    };

    var normalizePosition = function (forward, pos) {
      var container = pos.container(), offset = pos.offset();

      if (forward) {
        if (CaretContainer.isCaretContainerInline(container)) {
          if (NodeType.isText(container.nextSibling)) {
            return new CaretPosition(container.nextSibling, 0);
          } else {
            return CaretPosition.after(container);
          }
        } else {
          return CaretContainer.isBeforeInline(pos) ? new CaretPosition(container, offset + 1) : pos;
        }
      } else {
        if (CaretContainer.isCaretContainerInline(container)) {
          if (NodeType.isText(container.previousSibling)) {
            return new CaretPosition(container.previousSibling, container.previousSibling.data.length);
          } else {
            return CaretPosition.before(container);
          }
        } else {
          return CaretContainer.isAfterInline(pos) ? new CaretPosition(container, offset - 1) : pos;
        }
      }
    };

    var normalizeForwards = Fun.curry(normalizePosition, true);
    var normalizeBackwards = Fun.curry(normalizePosition, false);

    return {
      isInlineTarget: isInlineTarget,
      findRootInline: findRootInline,
      isRtl: isRtl,
      isAtZwsp: isAtZwsp,
      normalizePosition: normalizePosition,
      normalizeForwards: normalizeForwards,
      normalizeBackwards: normalizeBackwards,
      hasSameParentBlock: hasSameParentBlock
    };
  }
);
/**
 * BoundaryCaret.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
 */

define(
  'tinymce.core.keyboard.BoundaryCaret',
  [
    'ephox.katamari.api.Option',
    'tinymce.core.caret.CaretContainer',
    'tinymce.core.caret.CaretContainerInline',
    'tinymce.core.caret.CaretContainerRemove',
    'tinymce.core.caret.CaretFinder',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.dom.NodeType',
    'tinymce.core.keyboard.InlineUtils'
  ],
  function (Option, CaretContainer, CaretContainerInline, CaretContainerRemove, CaretFinder, CaretPosition, NodeType, InlineUtils) {
    var insertInlinePos = function (pos, before) {
      if (NodeType.isText(pos.container())) {
        return CaretContainerInline.insertInline(before, pos.container());
      } else {
        return CaretContainerInline.insertInline(before, pos.getNode());
      }
    };

    var isPosCaretContainer = function (pos, caret) {
      var caretNode = caret.get();
      return caretNode && pos.container() === caretNode && CaretContainer.isCaretContainerInline(caretNode);
    };

    var renderCaret = function (caret, location) {
      return location.fold(
        function (element) { // Before
          CaretContainerRemove.remove(caret.get());
          var text = CaretContainerInline.insertInlineBefore(element);
          caret.set(text);
          return Option.some(new CaretPosition(text, text.length - 1));
        },
        function (element) { // Start
          return CaretFinder.firstPositionIn(element).map(function (pos) {
            if (!isPosCaretContainer(pos, caret)) {
              CaretContainerRemove.remove(caret.get());
              var text = insertInlinePos(pos, true);
              caret.set(text);
              return new CaretPosition(text, 1);
            } else {
              return new CaretPosition(caret.get(), 1);
            }
          });
        },
        function (element) { // End
          return CaretFinder.lastPositionIn(element).map(function (pos) {
            if (!isPosCaretContainer(pos, caret)) {
              CaretContainerRemove.remove(caret.get());
              var text = insertInlinePos(pos, false);
              caret.set(text);
              return new CaretPosition(text, text.length - 1);
            } else {
              return new CaretPosition(caret.get(), caret.get().length - 1);
            }
          });
        },
        function (element) { // After
          CaretContainerRemove.remove(caret.get());
          var text = CaretContainerInline.insertInlineAfter(element);
          caret.set(text);
          return Option.some(new CaretPosition(text, 1));
        }
      );
    };

    return {
      renderCaret: renderCaret
    };
  }
);
/**
 * LazyEvaluator.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
 */

define(
  'tinymce.core.util.LazyEvaluator',
  [
    'ephox.katamari.api.Option'
  ],
  function (Option) {
    var evaluateUntil = function (fns, args) {
      for (var i = 0; i < fns.length; i++) {
        var result = fns[i].apply(null, args);
        if (result.isSome()) {
          return result;
        }
      }

      return Option.none();
    };

    return {
      evaluateUntil: evaluateUntil
    };
  }
);
/**
 * BoundaryLocation.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
 */

define(
  'tinymce.core.keyboard.BoundaryLocation',
  [
    'ephox.katamari.api.Adt',
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'ephox.katamari.api.Options',
    'tinymce.core.caret.CaretContainer',
    'tinymce.core.caret.CaretFinder',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.caret.CaretUtils',
    'tinymce.core.dom.NodeType',
    'tinymce.core.keyboard.InlineUtils',
    'tinymce.core.util.LazyEvaluator'
  ],
  function (Adt, Fun, Option, Options, CaretContainer, CaretFinder, CaretPosition, CaretUtils, NodeType, InlineUtils, LazyEvaluator) {
    var Location = Adt.generate([
      { before: [ 'element' ] },
      { start: [ 'element' ] },
      { end: [ 'element' ] },
      { after: [ 'element' ] }
    ]);

    var rescope = function (rootNode, node) {
      var parentBlock = CaretUtils.getParentBlock(node, rootNode);
      return parentBlock ? parentBlock : rootNode;
    };

    var before = function (isInlineTarget, rootNode, pos) {
      var nPos = InlineUtils.normalizeForwards(pos);
      var scope = rescope(rootNode, nPos.container());
      return InlineUtils.findRootInline(isInlineTarget, scope, nPos).fold(
        function () {
          return CaretFinder.nextPosition(scope, nPos)
            .bind(Fun.curry(InlineUtils.findRootInline, isInlineTarget, scope))
            .map(function (inline) {
              return Location.before(inline);
            });
        },
        Option.none
      );
    };

    var start = function (isInlineTarget, rootNode, pos) {
      var nPos = InlineUtils.normalizeBackwards(pos);
      return InlineUtils.findRootInline(isInlineTarget, rootNode, nPos).bind(function (inline) {
        var prevPos = CaretFinder.prevPosition(inline, nPos);
        return prevPos.isNone() ? Option.some(Location.start(inline)) : Option.none();
      });
    };

    var end = function (isInlineTarget, rootNode, pos) {
      var nPos = InlineUtils.normalizeForwards(pos);
      return InlineUtils.findRootInline(isInlineTarget, rootNode, nPos).bind(function (inline) {
        var nextPos = CaretFinder.nextPosition(inline, nPos);
        return nextPos.isNone() ? Option.some(Location.end(inline)) : Option.none();
      });
    };

    var after = function (isInlineTarget, rootNode, pos) {
      var nPos = InlineUtils.normalizeBackwards(pos);
      var scope = rescope(rootNode, nPos.container());
      return InlineUtils.findRootInline(isInlineTarget, scope, nPos).fold(
        function () {
          return CaretFinder.prevPosition(scope, nPos)
            .bind(Fun.curry(InlineUtils.findRootInline, isInlineTarget, scope))
            .map(function (inline) {
              return Location.after(inline);
            });
        },
        Option.none
      );
    };

    var isValidLocation = function (location) {
      return InlineUtils.isRtl(getElement(location)) === false;
    };

    var readLocation = function (isInlineTarget, rootNode, pos) {
      var location = LazyEvaluator.evaluateUntil([
        before,
        start,
        end,
        after
      ], [isInlineTarget, rootNode, pos]);

      return location.filter(isValidLocation);
    };

    var getElement = function (location) {
      return location.fold(
        Fun.identity, // Before
        Fun.identity, // Start
        Fun.identity, // End
        Fun.identity  // After
      );
    };

    var getName = function (location) {
      return location.fold(
        Fun.constant('before'), // Before
        Fun.constant('start'),  // Start
        Fun.constant('end'),    // End
        Fun.constant('after')   // After
      );
    };

    var outside = function (location) {
      return location.fold(
        Location.before, // Before
        Location.before, // Start
        Location.after,  // End
        Location.after   // After
      );
    };

    var inside = function (location) {
      return location.fold(
        Location.start, // Before
        Location.start, // Start
        Location.end,   // End
        Location.end    // After
      );
    };

    var isEq = function (location1, location2) {
      return getName(location1) === getName(location2) && getElement(location1) === getElement(location2);
    };

    var betweenInlines = function (forward, isInlineTarget, rootNode, from, to, location) {
      return Options.liftN([
        InlineUtils.findRootInline(isInlineTarget, rootNode, from),
        InlineUtils.findRootInline(isInlineTarget, rootNode, to)
      ], function (fromInline, toInline) {
        if (fromInline !== toInline && InlineUtils.hasSameParentBlock(rootNode, fromInline, toInline)) {
          // Force after since some browsers normalize and lean left into the closest inline
          return Location.after(forward ? fromInline : toInline);
        } else {
          return location;
        }
      }).getOr(location);
    };

    var skipNoMovement = function (fromLocation, toLocation) {
      return fromLocation.fold(
        Fun.constant(true),
        function (fromLocation) {
          return !isEq(fromLocation, toLocation);
        }
      );
    };

    var findLocationTraverse = function (forward, isInlineTarget, rootNode, fromLocation, pos) {
      var from = InlineUtils.normalizePosition(forward, pos);
      var to = CaretFinder.fromPosition(forward, rootNode, from).map(Fun.curry(InlineUtils.normalizePosition, forward));

      var location = to.fold(
        function () {
          return fromLocation.map(outside);
        },
        function (to) {
          return readLocation(isInlineTarget, rootNode, to)
            .map(Fun.curry(betweenInlines, forward, isInlineTarget, rootNode, from, to))
            .filter(Fun.curry(skipNoMovement, fromLocation));
        }
      );

      return location.filter(isValidLocation);
    };

    var findLocationSimple = function (forward, location) {
      if (forward) {
        return location.fold(
          Fun.compose(Option.some, Location.start), // Before -> Start
          Option.none,
          Fun.compose(Option.some, Location.after), // End -> After
          Option.none
        );
      } else {
        return location.fold(
          Option.none,
          Fun.compose(Option.some, Location.before), // Before <- Start
          Option.none,
          Fun.compose(Option.some, Location.end) // End <- After
        );
      }
    };

    var findLocation = function (forward, isInlineTarget, rootNode, pos) {
      var from = InlineUtils.normalizePosition(forward, pos);
      var fromLocation = readLocation(isInlineTarget, rootNode, from);

      return readLocation(isInlineTarget, rootNode, from).bind(Fun.curry(findLocationSimple, forward)).orThunk(function () {
        return findLocationTraverse(forward, isInlineTarget, rootNode, fromLocation, pos);
      });
    };

    return {
      readLocation: readLocation,
      findLocation: findLocation,
      prevLocation: Fun.curry(findLocation, false),
      nextLocation: Fun.curry(findLocation, true),
      getElement: getElement,
      outside: outside,
      inside: inside
    };
  }
);
/**
 * BoundarySelection.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
 */

define(
  'tinymce.core.keyboard.BoundarySelection',
  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Cell',
    'ephox.katamari.api.Fun',
    'tinymce.core.caret.CaretContainerRemove',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.keyboard.BoundaryCaret',
    'tinymce.core.keyboard.BoundaryLocation',
    'tinymce.core.keyboard.InlineUtils'
  ],
  function (Arr, Cell, Fun, CaretContainerRemove, CaretPosition, BoundaryCaret, BoundaryLocation, InlineUtils) {
    var setCaretPosition = function (editor, pos) {
      var rng = editor.dom.createRng();
      rng.setStart(pos.container(), pos.offset());
      rng.setEnd(pos.container(), pos.offset());
      editor.selection.setRng(rng);
    };

    var isFeatureEnabled = function (editor) {
      return editor.settings.inline_boundaries !== false;
    };

    var setSelected = function (state, elm) {
      if (state) {
        elm.setAttribute('data-mce-selected', '1');
      } else {
        elm.removeAttribute('data-mce-selected', '1');
      }
    };

    var renderCaretLocation = function (editor, caret, location) {
      return BoundaryCaret.renderCaret(caret, location).map(function (pos) {
        setCaretPosition(editor, pos);
        return location;
      });
    };

    var findLocation = function (editor, caret, forward) {
      var rootNode = editor.getBody();
      var from = CaretPosition.fromRangeStart(editor.selection.getRng());
      var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor);
      var location = BoundaryLocation.findLocation(forward, isInlineTarget, rootNode, from);
      return location.bind(function (location) {
        return renderCaretLocation(editor, caret, location);
      });
    };

    var toggleInlines = function (isInlineTarget, dom, elms) {
      var selectedInlines = Arr.filter(dom.select('*[data-mce-selected]'), isInlineTarget);
      var targetInlines = Arr.filter(elms, isInlineTarget);
      Arr.each(Arr.difference(selectedInlines, targetInlines), Fun.curry(setSelected, false));
      Arr.each(Arr.difference(targetInlines, selectedInlines), Fun.curry(setSelected, true));
    };

    var safeRemoveCaretContainer = function (editor, caret) {
      if (editor.selection.isCollapsed() && editor.composing !== true && caret.get()) {
        var pos = CaretPosition.fromRangeStart(editor.selection.getRng());
        if (CaretPosition.isTextPosition(pos) && InlineUtils.isAtZwsp(pos) === false) {
          setCaretPosition(editor, CaretContainerRemove.removeAndReposition(caret.get(), pos));
          caret.set(null);
        }
      }
    };

    var renderInsideInlineCaret = function (isInlineTarget, editor, caret, elms) {
      if (editor.selection.isCollapsed()) {
        var inlines = Arr.filter(elms, isInlineTarget);
        Arr.each(inlines, function (inline) {
          var pos = CaretPosition.fromRangeStart(editor.selection.getRng());
          BoundaryLocation.readLocation(isInlineTarget, editor.getBody(), pos).bind(function (location) {
            return renderCaretLocation(editor, caret, location);
          });
        });
      }
    };

    var move = function (editor, caret, forward) {
      return function () {
        return isFeatureEnabled(editor) ? findLocation(editor, caret, forward).isSome() : false;
      };
    };

    var setupSelectedState = function (editor) {
      var caret = new Cell(null);
      var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor);

      editor.on('NodeChange', function (e) {
        if (isFeatureEnabled(editor)) {
          toggleInlines(isInlineTarget, editor.dom, e.parents);
          safeRemoveCaretContainer(editor, caret);
          renderInsideInlineCaret(isInlineTarget, editor, caret, e.parents);
        }
      });

      return caret;
    };

    return {
      move: move,
      setupSelectedState: setupSelectedState,
      setCaretPosition: setCaretPosition
    };
  }
);
/**
 * InlineBoundaryDelete.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
 */

define(
  'tinymce.core.delete.InlineBoundaryDelete',
  [
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'ephox.katamari.api.Options',
    'ephox.sugar.api.node.Element',
    'global!document',
    'tinymce.core.caret.CaretContainer',
    'tinymce.core.caret.CaretFinder',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.caret.CaretUtils',
    'tinymce.core.delete.DeleteElement',
    'tinymce.core.keyboard.BoundaryCaret',
    'tinymce.core.keyboard.BoundaryLocation',
    'tinymce.core.keyboard.BoundarySelection',
    'tinymce.core.keyboard.InlineUtils'
  ],
  function (
    Fun, Option, Options, Element, document, CaretContainer, CaretFinder, CaretPosition, CaretUtils, DeleteElement, BoundaryCaret, BoundaryLocation, BoundarySelection,
    InlineUtils
  ) {
    var isFeatureEnabled = function (editor) {
      return editor.settings.inline_boundaries !== false;
    };

    var rangeFromPositions = function (from, to) {
      var range = document.createRange();

      range.setStart(from.container(), from.offset());
      range.setEnd(to.container(), to.offset());

      return range;
    };

    // Checks for delete at <code>|a</code> when there is only one item left except the zwsp caret container nodes
    var hasOnlyTwoOrLessPositionsLeft = function (elm) {
      return Options.liftN([
        CaretFinder.firstPositionIn(elm),
        CaretFinder.lastPositionIn(elm)
      ], function (firstPos, lastPos) {
        var normalizedFirstPos = InlineUtils.normalizePosition(true, firstPos);
        var normalizedLastPos = InlineUtils.normalizePosition(false, lastPos);

        return CaretFinder.nextPosition(elm, normalizedFirstPos).map(function (pos) {
          return pos.isEqual(normalizedLastPos);
        }).getOr(true);
      }).getOr(true);
    };

    var setCaretLocation = function (editor, caret) {
      return function (location) {
        return BoundaryCaret.renderCaret(caret, location).map(function (pos) {
          BoundarySelection.setCaretPosition(editor, pos);
          return true;
        }).getOr(false);
      };
    };

    var deleteFromTo = function (editor, caret, from, to) {
      var rootNode = editor.getBody();
      var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor);

      editor.undoManager.ignore(function () {
        editor.selection.setRng(rangeFromPositions(from, to));
        editor.execCommand('Delete');

        BoundaryLocation.readLocation(isInlineTarget, rootNode, CaretPosition.fromRangeStart(editor.selection.getRng()))
          .map(BoundaryLocation.inside)
          .map(setCaretLocation(editor, caret));
      });

      editor.nodeChanged();
    };

    var rescope = function (rootNode, node) {
      var parentBlock = CaretUtils.getParentBlock(node, rootNode);
      return parentBlock ? parentBlock : rootNode;
    };

    var backspaceDeleteCollapsed = function (editor, caret, forward, from) {
      var rootNode = rescope(editor.getBody(), from.container());
      var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor);
      var fromLocation = BoundaryLocation.readLocation(isInlineTarget, rootNode, from);

      return fromLocation.bind(function (location) {
        if (forward) {
          return location.fold(
            Fun.constant(Option.some(BoundaryLocation.inside(location))), // Before
            Option.none, // Start
            Fun.constant(Option.some(BoundaryLocation.outside(location))), // End
            Option.none  // After
          );
        } else {
          return location.fold(
            Option.none, // Before
            Fun.constant(Option.some(BoundaryLocation.outside(location))), // Start
            Option.none, // End
            Fun.constant(Option.some(BoundaryLocation.inside(location)))  // After
          );
        }
      })
      .map(setCaretLocation(editor, caret))
      .getOrThunk(function () {
        var toPosition = CaretFinder.navigate(forward, rootNode, from);
        var toLocation = toPosition.bind(function (pos) {
          return BoundaryLocation.readLocation(isInlineTarget, rootNode, pos);
        });

        if (fromLocation.isSome() && toLocation.isSome()) {
          return InlineUtils.findRootInline(isInlineTarget, rootNode, from).map(function (elm) {
            if (hasOnlyTwoOrLessPositionsLeft(elm)) {
              DeleteElement.deleteElement(editor, forward, Element.fromDom(elm));
              return true;
            } else {
              return false;
            }
          }).getOr(false);
        } else {
          return toLocation.bind(function (_) {
            return toPosition.map(function (to) {
              if (forward) {
                deleteFromTo(editor, caret, from, to);
              } else {
                deleteFromTo(editor, caret, to, from);
              }

              return true;
            });
          }).getOr(false);
        }
      });
    };

    var backspaceDelete = function (editor, caret, forward) {
      if (editor.selection.isCollapsed() && isFeatureEnabled(editor)) {
        var from = CaretPosition.fromRangeStart(editor.selection.getRng());
        return backspaceDeleteCollapsed(editor, caret, forward, from);
      }

      return false;
    };

    return {
      backspaceDelete: backspaceDelete
    };
  }
);
define(
  'tinymce.core.delete.TableDeleteAction',

  [
    'ephox.katamari.api.Adt',
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'ephox.katamari.api.Options',
    'ephox.katamari.api.Struct',
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.search.SelectorFilter',
    'ephox.sugar.api.search.SelectorFind'
  ],

  function (Adt, Arr, Fun, Option, Options, Struct, Compare, Element, SelectorFilter, SelectorFind) {
    var tableCellRng = Struct.immutable('start', 'end');
    var tableSelection = Struct.immutable('rng', 'table', 'cells');
    var deleteAction = Adt.generate([
      { removeTable: [ 'element' ] },
      { emptyCells: [ 'cells' ] }
    ]);

    var getClosestCell = function (container, isRoot) {
      return SelectorFind.closest(Element.fromDom(container), 'td,th', isRoot);
    };

    var getClosestTable = function (cell, isRoot) {
      return SelectorFind.ancestor(cell, 'table', isRoot);
    };

    var isExpandedCellRng = function (cellRng) {
      return Compare.eq(cellRng.start(), cellRng.end()) === false;
    };

    var getTableFromCellRng = function (cellRng, isRoot) {
      return getClosestTable(cellRng.start(), isRoot)
        .bind(function (startParentTable) {
          return getClosestTable(cellRng.end(), isRoot)
            .bind(function (endParentTable) {
              return Compare.eq(startParentTable, endParentTable) ? Option.some(startParentTable) : Option.none();
            });
        });
    };

    var getCellRng = function (rng, isRoot) {
      return Options.liftN([ // get start and end cell
        getClosestCell(rng.startContainer, isRoot),
        getClosestCell(rng.endContainer, isRoot)
      ], tableCellRng)
        .filter(isExpandedCellRng);
    };

    var getTableSelectionFromCellRng = function (cellRng, isRoot) {
      return getTableFromCellRng(cellRng, isRoot)
        .bind(function (table) {
          var cells = SelectorFilter.descendants(table, 'td,th');

          return tableSelection(cellRng, table, cells);
        });
    };

    var getTableSelectionFromRng = function (rootNode, rng) {
      var isRoot = Fun.curry(Compare.eq, rootNode);

      return getCellRng(rng, isRoot)
        .map(function (cellRng) {
          return getTableSelectionFromCellRng(cellRng, isRoot);
        });
    };

    var getCellIndex = function (cellArray, cell) {
      return Arr.findIndex(cellArray, function (x) {
        return Compare.eq(x, cell);
      });
    };

    var getSelectedCells = function (tableSelection) {
      return Options.liftN([
        getCellIndex(tableSelection.cells(), tableSelection.rng().start()),
        getCellIndex(tableSelection.cells(), tableSelection.rng().end())
      ], function (startIndex, endIndex) {
        return tableSelection.cells().slice(startIndex, endIndex + 1);
      });
    };

    var getAction = function (tableSelection) {
      return getSelectedCells(tableSelection)
        .bind(function (selected) {
          var cells = tableSelection.cells();

          return selected.length === cells.length ? deleteAction.removeTable(tableSelection.table()) : deleteAction.emptyCells(selected);
        });
    };

    var getActionFromCells = function (cells) {
      return deleteAction.emptyCells(cells);
    };

    var getActionFromRange = function (rootNode, rng) {
      return getTableSelectionFromRng(rootNode, rng)
        .map(getAction);
    };

    return {
      getActionFromRange: getActionFromRange,
      getActionFromCells: getActionFromCells
    };
  }
);

/**
 * TableDelete.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
 */

define(
  'tinymce.core.delete.TableDelete',
  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.node.Node',
    'tinymce.core.caret.CaretFinder',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.delete.DeleteElement',
    'tinymce.core.delete.TableDeleteAction',
    'tinymce.core.dom.ElementType',
    'tinymce.core.dom.Empty',
    'tinymce.core.dom.PaddingBr',
    'tinymce.core.dom.Parents',
    'tinymce.core.selection.TableCellSelection'
  ],
  function (Arr, Fun, Option, Compare, Element, Node, CaretFinder, CaretPosition, DeleteElement, TableDeleteAction, ElementType, Empty, PaddingBr, Parents, TableCellSelection) {
    var emptyCells = function (editor, cells) {
      Arr.each(cells, PaddingBr.fillWithPaddingBr);
      editor.selection.setCursorLocation(cells[0].dom(), 0);

      return true;
    };

    var deleteTableElement = function (editor, table) {
      DeleteElement.deleteElement(editor, false, table);

      return true;
    };

    var deleteCellRange = function (editor, rootElm, rng) {
      return TableDeleteAction.getActionFromRange(rootElm, rng).map(function (action) {
        return action.fold(
          Fun.curry(deleteTableElement, editor),
          Fun.curry(emptyCells, editor)
        );
      });
    };

    var deleteCaptionRange = function (editor, caption) {
      return emptyElement(editor, caption);
    };

    var deleteTableRange = function (editor, rootElm, rng, startElm) {
      return getParentCaption(rootElm, startElm).fold(
        function () {
          return deleteCellRange(editor, rootElm, rng);
        },
        function (caption) {
          return deleteCaptionRange(editor, caption);
        }
      ).getOr(false);
    };

    var deleteRange = function (editor, startElm) {
      var rootNode = Element.fromDom(editor.getBody());
      var rng = editor.selection.getRng();
      var selectedCells = TableCellSelection.getCellsFromEditor(editor);

      return selectedCells.length !== 0 ? emptyCells(editor, selectedCells) : deleteTableRange(editor, rootNode, rng, startElm);
    };

    var getParentCell = function (rootElm, elm) {
      return Arr.find(Parents.parentsAndSelf(elm, rootElm), ElementType.isTableCell);
    };

    var getParentCaption = function (rootElm, elm) {
      return Arr.find(Parents.parentsAndSelf(elm, rootElm), function (elm) {
        return Node.name(elm) === 'caption';
      });
    };

    var deleteBetweenCells = function (editor, rootElm, forward, fromCell, from) {
      return CaretFinder.navigate(forward, editor.getBody(), from).bind(function (to) {
        return getParentCell(rootElm, Element.fromDom(to.getNode())).map(function (toCell) {
          return Compare.eq(toCell, fromCell) === false;
        });
      });
    };

    var emptyElement = function (editor, elm) {
      PaddingBr.fillWithPaddingBr(elm);
      editor.selection.setCursorLocation(elm.dom(), 0);
      return Option.some(true);
    };

    var isDeleteOfLastCharPos = function (fromCaption, forward, from, to) {
      return CaretFinder.firstPositionIn(fromCaption.dom()).bind(function (first) {
        return CaretFinder.lastPositionIn(fromCaption.dom()).map(function (last) {
          return forward ? from.isEqual(first) && to.isEqual(last) : from.isEqual(last) && to.isEqual(first);
        });
      }).getOr(true);
    };

    var emptyCaretCaption = function (editor, elm) {
      return emptyElement(editor, elm);
    };

    var validateCaretCaption = function (rootElm, fromCaption, to) {
      return getParentCaption(rootElm, Element.fromDom(to.getNode())).map(function (toCaption) {
        return Compare.eq(toCaption, fromCaption) === false;
      });
    };

    var deleteCaretInsideCaption = function (editor, rootElm, forward, fromCaption, from) {
      return CaretFinder.navigate(forward, editor.getBody(), from).bind(function (to) {
        return isDeleteOfLastCharPos(fromCaption, forward, from, to) ? emptyCaretCaption(editor, fromCaption) : validateCaretCaption(rootElm, fromCaption, to);
      }).or(Option.some(true));
    };

    var deleteCaretCells = function (editor, forward, rootElm, startElm) {
      var from = CaretPosition.fromRangeStart(editor.selection.getRng());
      return getParentCell(rootElm, startElm).bind(function (fromCell) {
        return Empty.isEmpty(fromCell) ? emptyElement(editor, fromCell) : deleteBetweenCells(editor, rootElm, forward, fromCell, from);
      });
    };

    var deleteCaretCaption = function (editor, forward, rootElm, fromCaption) {
      var from = CaretPosition.fromRangeStart(editor.selection.getRng());
      return Empty.isEmpty(fromCaption) ? emptyElement(editor, fromCaption) : deleteCaretInsideCaption(editor, rootElm, forward, fromCaption, from);
    };

    var deleteCaret = function (editor, forward, startElm) {
      var rootElm = Element.fromDom(editor.getBody());

      return getParentCaption(rootElm, startElm).fold(
        function () {
          return deleteCaretCells(editor, forward, rootElm, startElm);
        },
        function (fromCaption) {
          return deleteCaretCaption(editor, forward, rootElm, fromCaption);
        }
      ).getOr(false);
    };

    var backspaceDelete = function (editor, forward) {
      var startElm = Element.fromDom(editor.selection.getStart(true));
      return editor.selection.isCollapsed() ? deleteCaret(editor, forward, startElm) : deleteRange(editor, startElm);
    };

    return {
      backspaceDelete: backspaceDelete
    };
  }
);

/**
 * Commands.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
 */

define(
  'tinymce.core.delete.DeleteCommands',
  [
    'tinymce.core.delete.BlockBoundaryDelete',
    'tinymce.core.delete.BlockRangeDelete',
    'tinymce.core.delete.CefDelete',
    'tinymce.core.delete.DeleteUtils',
    'tinymce.core.delete.InlineBoundaryDelete',
    'tinymce.core.delete.TableDelete'
  ],
  function (BlockBoundaryDelete, BlockRangeDelete, CefDelete, DeleteUtils, BoundaryDelete, TableDelete) {
    var nativeCommand = function (editor, command) {
      editor.getDoc().execCommand(command, false, null);
    };

    var deleteCommand = function (editor) {
      if (CefDelete.backspaceDelete(editor, false)) {
        return;
      } else if (BoundaryDelete.backspaceDelete(editor, false)) {
        return;
      } else if (BlockBoundaryDelete.backspaceDelete(editor, false)) {
        return;
      } else if (TableDelete.backspaceDelete(editor)) {
        return;
      } else if (BlockRangeDelete.backspaceDelete(editor, false)) {
        return;
      } else {
        nativeCommand(editor, 'Delete');
        DeleteUtils.paddEmptyBody(editor);
      }
    };

    var forwardDeleteCommand = function (editor) {
      if (CefDelete.backspaceDelete(editor, true)) {
        return;
      } else if (BoundaryDelete.backspaceDelete(editor, true)) {
        return;
      } else if (BlockBoundaryDelete.backspaceDelete(editor, true)) {
        return;
      } else if (TableDelete.backspaceDelete(editor)) {
        return;
      } else if (BlockRangeDelete.backspaceDelete(editor, true)) {
        return;
      } else {
        nativeCommand(editor, 'ForwardDelete');
      }
    };

    return {
      deleteCommand: deleteCommand,
      forwardDeleteCommand: forwardDeleteCommand
    };
  }
);
/**
 * EditorCommands.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
 */

/**
 * This class enables you to add custom editor commands and it contains
 * overrides for native browser commands to address various bugs and issues.
 *
 * @class tinymce.EditorCommands
 */
define(
  'tinymce.core.EditorCommands',
  [
    'tinymce.core.Env',
    'tinymce.core.InsertContent',
    'tinymce.core.delete.DeleteCommands',
    'tinymce.core.dom.NodeType',
    'tinymce.core.dom.RangeUtils',
    'tinymce.core.dom.TreeWalker',
    'tinymce.core.selection.SelectionBookmark',
    'tinymce.core.util.Tools'
  ],
  function (Env, InsertContent, DeleteCommands, NodeType, RangeUtils, TreeWalker, SelectionBookmark, Tools) {
    // Added for compression purposes
    var each = Tools.each, extend = Tools.extend;
    var map = Tools.map, inArray = Tools.inArray, explode = Tools.explode;
    var isOldIE = Env.ie && Env.ie < 11;
    var TRUE = true, FALSE = false;

    return function (editor) {
      var dom, selection, formatter,
        commands = { state: {}, exec: {}, value: {} },
        settings = editor.settings,
        bookmark;

      editor.on('PreInit', function () {
        dom = editor.dom;
        selection = editor.selection;
        settings = editor.settings;
        formatter = editor.formatter;
      });

      /**
       * Executes the specified command.
       *
       * @method execCommand
       * @param {String} command Command to execute.
       * @param {Boolean} ui Optional user interface state.
       * @param {Object} value Optional value for command.
       * @param {Object} args Optional extra arguments to the execCommand.
       * @return {Boolean} true/false if the command was found or not.
       */
      var execCommand = function (command, ui, value, args) {
        var func, customCommand, state = 0;

        if (editor.removed) {
          return;
        }

        if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint)$/.test(command) && (!args || !args.skip_focus)) {
          editor.focus();
        } else {
          SelectionBookmark.restore(editor);
        }

        args = editor.fire('BeforeExecCommand', { command: command, ui: ui, value: value });
        if (args.isDefaultPrevented()) {
          return false;
        }

        customCommand = command.toLowerCase();
        if ((func = commands.exec[customCommand])) {
          func(customCommand, ui, value);
          editor.fire('ExecCommand', { command: command, ui: ui, value: value });
          return true;
        }

        // Plugin commands
        each(editor.plugins, function (p) {
          if (p.execCommand && p.execCommand(command, ui, value)) {
            editor.fire('ExecCommand', { command: command, ui: ui, value: value });
            state = true;
            return false;
          }
        });

        if (state) {
          return state;
        }

        // Theme commands
        if (editor.theme && editor.theme.execCommand && editor.theme.execCommand(command, ui, value)) {
          editor.fire('ExecCommand', { command: command, ui: ui, value: value });
          return true;
        }

        // Browser commands
        try {
          state = editor.getDoc().execCommand(command, ui, value);
        } catch (ex) {
          // Ignore old IE errors
        }

        if (state) {
          editor.fire('ExecCommand', { command: command, ui: ui, value: value });
          return true;
        }

        return false;
      };

      /**
       * Queries the current state for a command for example if the current selection is "bold".
       *
       * @method queryCommandState
       * @param {String} command Command to check the state of.
       * @return {Boolean/Number} true/false if the selected contents is bold or not, -1 if it's not found.
       */
      var queryCommandState = function (command) {
        var func;

        if (editor.quirks.isHidden() || editor.removed) {
          return;
        }

        command = command.toLowerCase();
        if ((func = commands.state[command])) {
          return func(command);
        }

        // Browser commands
        try {
          return editor.getDoc().queryCommandState(command);
        } catch (ex) {
          // Fails sometimes see bug: 1896577
        }

        return false;
      };

      /**
       * Queries the command value for example the current fontsize.
       *
       * @method queryCommandValue
       * @param {String} command Command to check the value of.
       * @return {Object} Command value of false if it's not found.
       */
      var queryCommandValue = function (command) {
        var func;

        if (editor.quirks.isHidden() || editor.removed) {
          return;
        }

        command = command.toLowerCase();
        if ((func = commands.value[command])) {
          return func(command);
        }

        // Browser commands
        try {
          return editor.getDoc().queryCommandValue(command);
        } catch (ex) {
          // Fails sometimes see bug: 1896577
        }
      };

      /**
       * Adds commands to the command collection.
       *
       * @method addCommands
       * @param {Object} commandList Name/value collection with commands to add, the names can also be comma separated.
       * @param {String} type Optional type to add, defaults to exec. Can be value or state as well.
       */
      var addCommands = function (commandList, type) {
        type = type || 'exec';

        each(commandList, function (callback, command) {
          each(command.toLowerCase().split(','), function (command) {
            commands[type][command] = callback;
          });
        });
      };

      var addCommand = function (command, callback, scope) {
        command = command.toLowerCase();
        commands.exec[command] = function (command, ui, value, args) {
          return callback.call(scope || editor, ui, value, args);
        };
      };

      /**
       * Returns true/false if the command is supported or not.
       *
       * @method queryCommandSupported
       * @param {String} command Command that we check support for.
       * @return {Boolean} true/false if the command is supported or not.
       */
      var queryCommandSupported = function (command) {
        command = command.toLowerCase();

        if (commands.exec[command]) {
          return true;
        }

        // Browser commands
        try {
          return editor.getDoc().queryCommandSupported(command);
        } catch (ex) {
          // Fails sometimes see bug: 1896577
        }

        return false;
      };

      var addQueryStateHandler = function (command, callback, scope) {
        command = command.toLowerCase();
        commands.state[command] = function () {
          return callback.call(scope || editor);
        };
      };

      var addQueryValueHandler = function (command, callback, scope) {
        command = command.toLowerCase();
        commands.value[command] = function () {
          return callback.call(scope || editor);
        };
      };

      var hasCustomCommand = function (command) {
        command = command.toLowerCase();
        return !!commands.exec[command];
      };

      // Expose public methods
      extend(this, {
        execCommand: execCommand,
        queryCommandState: queryCommandState,
        queryCommandValue: queryCommandValue,
        queryCommandSupported: queryCommandSupported,
        addCommands: addCommands,
        addCommand: addCommand,
        addQueryStateHandler: addQueryStateHandler,
        addQueryValueHandler: addQueryValueHandler,
        hasCustomCommand: hasCustomCommand
      });

      // Private methods

      var execNativeCommand = function (command, ui, value) {
        if (ui === undefined) {
          ui = FALSE;
        }

        if (value === undefined) {
          value = null;
        }

        return editor.getDoc().execCommand(command, ui, value);
      };

      var isFormatMatch = function (name) {
        return formatter.match(name);
      };

      var toggleFormat = function (name, value) {
        formatter.toggle(name, value ? { value: value } : undefined);
        editor.nodeChanged();
      };

      var storeSelection = function (type) {
        bookmark = selection.getBookmark(type);
      };

      var restoreSelection = function () {
        selection.moveToBookmark(bookmark);
      };

      // Add execCommand overrides
      addCommands({
        // Ignore these, added for compatibility
        'mceResetDesignMode,mceBeginUndoLevel': function () { },

        // Add undo manager logic
        'mceEndUndoLevel,mceAddUndoLevel': function () {
          editor.undoManager.add();
        },

        'Cut,Copy,Paste': function (command) {
          var doc = editor.getDoc(), failed;

          // Try executing the native command
          try {
            execNativeCommand(command);
          } catch (ex) {
            // Command failed
            failed = TRUE;
          }

          // Chrome reports the paste command as supported however older IE:s will return false for cut/paste
          if (command === 'paste' && !doc.queryCommandEnabled(command)) {
            failed = true;
          }

          // Present alert message about clipboard access not being available
          if (failed || !doc.queryCommandSupported(command)) {
            var msg = editor.translate(
              "Your browser doesn't support direct access to the clipboard. " +
              "Please use the Ctrl+X/C/V keyboard shortcuts instead."
            );

            if (Env.mac) {
              msg = msg.replace(/Ctrl\+/g, '\u2318+');
            }

            editor.notificationManager.open({ text: msg, type: 'error' });
          }
        },

        // Override unlink command
        unlink: function () {
          if (selection.isCollapsed()) {
            var elm = editor.dom.getParent(editor.selection.getStart(), 'a');
            if (elm) {
              editor.dom.remove(elm, true);
            }

            return;
          }

          formatter.remove("link");
        },

        // Override justify commands to use the text formatter engine
        'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull,JustifyNone': function (command) {
          var align = command.substring(7);

          if (align == 'full') {
            align = 'justify';
          }

          // Remove all other alignments first
          each('left,center,right,justify'.split(','), function (name) {
            if (align != name) {
              formatter.remove('align' + name);
            }
          });

          if (align != 'none') {
            toggleFormat('align' + align);
          }
        },

        // Override list commands to fix WebKit bug
        'InsertUnorderedList,InsertOrderedList': function (command) {
          var listElm, listParent;

          execNativeCommand(command);

          // WebKit produces lists within block elements so we need to split them
          // we will replace the native list creation logic to custom logic later on
          // TODO: Remove this when the list creation logic is removed
          listElm = dom.getParent(selection.getNode(), 'ol,ul');
          if (listElm) {
            listParent = listElm.parentNode;

            // If list is within a text block then split that block
            if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) {
              storeSelection();
              dom.split(listParent, listElm);
              restoreSelection();
            }
          }
        },

        // Override commands to use the text formatter engine
        'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function (command) {
          toggleFormat(command);
        },

        // Override commands to use the text formatter engine
        'ForeColor,HiliteColor,FontName': function (command, ui, value) {
          toggleFormat(command, value);
        },

        FontSize: function (command, ui, value) {
          var fontClasses, fontSizes;

          // Convert font size 1-7 to styles
          if (value >= 1 && value <= 7) {
            fontSizes = explode(settings.font_size_style_values);
            fontClasses = explode(settings.font_size_classes);

            if (fontClasses) {
              value = fontClasses[value - 1] || value;
            } else {
              value = fontSizes[value - 1] || value;
            }
          }

          toggleFormat(command, value);
        },

        RemoveFormat: function (command) {
          formatter.remove(command);
        },

        mceBlockQuote: function () {
          toggleFormat('blockquote');
        },

        FormatBlock: function (command, ui, value) {
          return toggleFormat(value || 'p');
        },

        mceCleanup: function () {
          var bookmark = selection.getBookmark();

          editor.setContent(editor.getContent({ cleanup: TRUE }), { cleanup: TRUE });

          selection.moveToBookmark(bookmark);
        },

        mceRemoveNode: function (command, ui, value) {
          var node = value || selection.getNode();

          // Make sure that the body node isn't removed
          if (node != editor.getBody()) {
            storeSelection();
            editor.dom.remove(node, TRUE);
            restoreSelection();
          }
        },

        mceSelectNodeDepth: function (command, ui, value) {
          var counter = 0;

          dom.getParent(selection.getNode(), function (node) {
            if (node.nodeType == 1 && counter++ == value) {
              selection.select(node);
              return FALSE;
            }
          }, editor.getBody());
        },

        mceSelectNode: function (command, ui, value) {
          selection.select(value);
        },

        mceInsertContent: function (command, ui, value) {
          InsertContent.insertAtCaret(editor, value);
        },

        mceInsertRawHTML: function (command, ui, value) {
          selection.setContent('tiny_mce_marker');
          editor.setContent(
            editor.getContent().replace(/tiny_mce_marker/g, function () {
              return value;
            })
          );
        },

        mceToggleFormat: function (command, ui, value) {
          toggleFormat(value);
        },

        mceSetContent: function (command, ui, value) {
          editor.setContent(value);
        },

        'Indent,Outdent': function (command) {
          var intentValue, indentUnit, value;

          // Setup indent level
          intentValue = settings.indentation;
          indentUnit = /[a-z%]+$/i.exec(intentValue);
          intentValue = parseInt(intentValue, 10);

          if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) {
            // If forced_root_blocks is set to false we don't have a block to indent so lets create a div
            if (!settings.forced_root_block && !dom.getParent(selection.getNode(), dom.isBlock)) {
              formatter.apply('div');
            }

            each(selection.getSelectedBlocks(), function (element) {
              if (dom.getContentEditable(element) === "false") {
                return;
              }

              if (element.nodeName !== "LI") {
                var indentStyleName = editor.getParam('indent_use_margin', false) ? 'margin' : 'padding';
                indentStyleName = element.nodeName === 'TABLE' ? 'margin' : indentStyleName;
                indentStyleName += dom.getStyle(element, 'direction', true) == 'rtl' ? 'Right' : 'Left';

                if (command == 'outdent') {
                  value = Math.max(0, parseInt(element.style[indentStyleName] || 0, 10) - intentValue);
                  dom.setStyle(element, indentStyleName, value ? value + indentUnit : '');
                } else {
                  value = (parseInt(element.style[indentStyleName] || 0, 10) + intentValue) + indentUnit;
                  dom.setStyle(element, indentStyleName, value);
                }
              }
            });
          } else {
            execNativeCommand(command);
          }
        },

        mceRepaint: function () {
        },

        InsertHorizontalRule: function () {
          editor.execCommand('mceInsertContent', false, '<hr />');
        },

        mceToggleVisualAid: function () {
          editor.hasVisual = !editor.hasVisual;
          editor.addVisual();
        },

        mceReplaceContent: function (command, ui, value) {
          editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({ format: 'text' })));
        },

        mceInsertLink: function (command, ui, value) {
          var anchor;

          if (typeof value == 'string') {
            value = { href: value };
          }

          anchor = dom.getParent(selection.getNode(), 'a');

          // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here.
          value.href = value.href.replace(' ', '%20');

          // Remove existing links if there could be child links or that the href isn't specified
          if (!anchor || !value.href) {
            formatter.remove('link');
          }

          // Apply new link to selection
          if (value.href) {
            formatter.apply('link', value, anchor);
          }
        },

        selectAll: function () {
          var editingHost = dom.getParent(selection.getStart(), NodeType.isContentEditableTrue);
          if (editingHost) {
            var rng = dom.createRng();
            rng.selectNodeContents(editingHost);
            selection.setRng(rng);
          }
        },

        "delete": function () {
          DeleteCommands.deleteCommand(editor);
        },

        "forwardDelete": function () {
          DeleteCommands.forwardDeleteCommand(editor);
        },

        mceNewDocument: function () {
          editor.setContent('');
        },

        InsertLineBreak: function (command, ui, value) {
          // We load the current event in from EnterKey.js when appropriate to heed
          // certain event-specific variations such as ctrl-enter in a list
          var evt = value;
          var brElm, extraBr, marker;
          var rng = selection.getRng(true);
          new RangeUtils(dom).normalize(rng);

          var offset = rng.startOffset;
          var container = rng.startContainer;

          // Resolve node index
          if (container.nodeType == 1 && container.hasChildNodes()) {
            var isAfterLastNodeInContainer = offset > container.childNodes.length - 1;

            container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container;
            if (isAfterLastNodeInContainer && container.nodeType == 3) {
              offset = container.nodeValue.length;
            } else {
              offset = 0;
            }
          }

          var parentBlock = dom.getParent(container, dom.isBlock);
          var parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
          var containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null;
          var containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5

          // Enter inside block contained within a LI then split or insert before/after LI
          var isControlKey = evt && evt.ctrlKey;
          if (containerBlockName == 'LI' && !isControlKey) {
            parentBlock = containerBlock;
            parentBlockName = containerBlockName;
          }

          // Walks the parent block to the right and look for BR elements
          var hasRightSideContent = function () {
            var walker = new TreeWalker(container, parentBlock), node;
            var nonEmptyElementsMap = editor.schema.getNonEmptyElements();

            while ((node = walker.next())) {
              if (nonEmptyElementsMap[node.nodeName.toLowerCase()] || node.length > 0) {
                return true;
              }
            }
          };

          if (container && container.nodeType == 3 && offset >= container.nodeValue.length) {
            // Insert extra BR element at the end block elements
            if (!isOldIE && !hasRightSideContent()) {
              brElm = dom.create('br');
              rng.insertNode(brElm);
              rng.setStartAfter(brElm);
              rng.setEndAfter(brElm);
              extraBr = true;
            }
          }

          brElm = dom.create('br');
          rng.insertNode(brElm);

          // Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it
          var documentMode = dom.doc.documentMode;
          if (isOldIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) {
            brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm);
          }

          // Insert temp marker and scroll to that
          marker = dom.create('span', {}, '&nbsp;');
          brElm.parentNode.insertBefore(marker, brElm);
          selection.scrollIntoView(marker);
          dom.remove(marker);

          if (!extraBr) {
            rng.setStartAfter(brElm);
            rng.setEndAfter(brElm);
          } else {
            rng.setStartBefore(brElm);
            rng.setEndBefore(brElm);
          }

          selection.setRng(rng);
          editor.undoManager.add();

          return TRUE;
        }
      });

      // Add queryCommandState overrides
      addCommands({
        // Override justify commands
        'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull': function (command) {
          var name = 'align' + command.substring(7);
          var nodes = selection.isCollapsed() ? [dom.getParent(selection.getNode(), dom.isBlock)] : selection.getSelectedBlocks();
          var matches = map(nodes, function (node) {
            return !!formatter.matchNode(node, name);
          });
          return inArray(matches, TRUE) !== -1;
        },

        'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function (command) {
          return isFormatMatch(command);
        },

        mceBlockQuote: function () {
          return isFormatMatch('blockquote');
        },

        Outdent: function () {
          var node;

          if (settings.inline_styles) {
            if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) {
              return TRUE;
            }

            if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) {
              return TRUE;
            }
          }

          return (
            queryCommandState('InsertUnorderedList') ||
            queryCommandState('InsertOrderedList') ||
            (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE'))
          );
        },

        'InsertUnorderedList,InsertOrderedList': function (command) {
          var list = dom.getParent(selection.getNode(), 'ul,ol');

          return list &&
            (
              command === 'insertunorderedlist' && list.tagName === 'UL' ||
              command === 'insertorderedlist' && list.tagName === 'OL'
            );
        }
      }, 'state');

      // Add queryCommandValue overrides
      addCommands({
        'FontSize,FontName': function (command) {
          var value = 0, parent;

          if ((parent = dom.getParent(selection.getNode(), 'span'))) {
            if (command == 'fontsize') {
              value = parent.style.fontSize;
            } else {
              value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase();
            }
          }

          return value;
        }
      }, 'value');

      // Add undo manager logic
      addCommands({
        Undo: function () {
          editor.undoManager.undo();
        },

        Redo: function () {
          editor.undoManager.redo();
        }
      });
    };
  }
);

/**
 * EventDispatcher.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
 */

/**
 * This class lets you add/remove and fire events by name on the specified scope. This makes
 * it easy to add event listener logic to any class.
 *
 * @class tinymce.util.EventDispatcher
 * @example
 *  var eventDispatcher = new EventDispatcher();
 *
 *  eventDispatcher.on('click', function() {console.log('data');});
 *  eventDispatcher.fire('click', {data: 123});
 */
define(
  'tinymce.core.util.EventDispatcher',
  [
    "tinymce.core.util.Tools"
  ],
  function (Tools) {
    var nativeEvents = Tools.makeMap(
      "focus blur focusin focusout click dblclick mousedown mouseup mousemove mouseover beforepaste paste cut copy selectionchange " +
      "mouseout mouseenter mouseleave wheel keydown keypress keyup input contextmenu dragstart dragend dragover " +
      "draggesture dragdrop drop drag submit " +
      "compositionstart compositionend compositionupdate touchstart touchmove touchend",
      ' '
    );

    var Dispatcher = function (settings) {
      var self = this, scope, bindings = {}, toggleEvent;

      var returnFalse = function () {
        return false;
      };

      var returnTrue = function () {
        return true;
      };

      settings = settings || {};
      scope = settings.scope || self;
      toggleEvent = settings.toggleEvent || returnFalse;

      /**
       * Fires the specified event by name.
       *
       * @method fire
       * @param {String} name Name of the event to fire.
       * @param {Object?} args Event arguments.
       * @return {Object} Event args instance passed in.
       * @example
       * instance.fire('event', {...});
       */
      var fire = function (name, args) {
        var handlers, i, l, callback;

        name = name.toLowerCase();
        args = args || {};
        args.type = name;

        // Setup target is there isn't one
        if (!args.target) {
          args.target = scope;
        }

        // Add event delegation methods if they are missing
        if (!args.preventDefault) {
          // Add preventDefault method
          args.preventDefault = function () {
            args.isDefaultPrevented = returnTrue;
          };

          // Add stopPropagation
          args.stopPropagation = function () {
            args.isPropagationStopped = returnTrue;
          };

          // Add stopImmediatePropagation
          args.stopImmediatePropagation = function () {
            args.isImmediatePropagationStopped = returnTrue;
          };

          // Add event delegation states
          args.isDefaultPrevented = returnFalse;
          args.isPropagationStopped = returnFalse;
          args.isImmediatePropagationStopped = returnFalse;
        }

        if (settings.beforeFire) {
          settings.beforeFire(args);
        }

        handlers = bindings[name];
        if (handlers) {
          for (i = 0, l = handlers.length; i < l; i++) {
            callback = handlers[i];

            // Unbind handlers marked with "once"
            if (callback.once) {
              off(name, callback.func);
            }

            // Stop immediate propagation if needed
            if (args.isImmediatePropagationStopped()) {
              args.stopPropagation();
              return args;
            }

            // If callback returns false then prevent default and stop all propagation
            if (callback.func.call(scope, args) === false) {
              args.preventDefault();
              return args;
            }
          }
        }

        return args;
      };

      /**
       * Binds an event listener to a specific event by name.
       *
       * @method on
       * @param {String} name Event name or space separated list of events to bind.
       * @param {callback} callback Callback to be executed when the event occurs.
       * @param {Boolean} first Optional flag if the event should be prepended. Use this with care.
       * @return {Object} Current class instance.
       * @example
       * instance.on('event', function(e) {
       *     // Callback logic
       * });
       */
      var on = function (name, callback, prepend, extra) {
        var handlers, names, i;

        if (callback === false) {
          callback = returnFalse;
        }

        if (callback) {
          callback = {
            func: callback
          };

          if (extra) {
            Tools.extend(callback, extra);
          }

          names = name.toLowerCase().split(' ');
          i = names.length;
          while (i--) {
            name = names[i];
            handlers = bindings[name];
            if (!handlers) {
              handlers = bindings[name] = [];
              toggleEvent(name, true);
            }

            if (prepend) {
              handlers.unshift(callback);
            } else {
              handlers.push(callback);
            }
          }
        }

        return self;
      };

      /**
       * Unbinds an event listener to a specific event by name.
       *
       * @method off
       * @param {String?} name Name of the event to unbind.
       * @param {callback?} callback Callback to unbind.
       * @return {Object} Current class instance.
       * @example
       * // Unbind specific callback
       * instance.off('event', handler);
       *
       * // Unbind all listeners by name
       * instance.off('event');
       *
       * // Unbind all events
       * instance.off();
       */
      var off = function (name, callback) {
        var i, handlers, bindingName, names, hi;

        if (name) {
          names = name.toLowerCase().split(' ');
          i = names.length;
          while (i--) {
            name = names[i];
            handlers = bindings[name];

            // Unbind all handlers
            if (!name) {
              for (bindingName in bindings) {
                toggleEvent(bindingName, false);
                delete bindings[bindingName];
              }

              return self;
            }

            if (handlers) {
              // Unbind all by name
              if (!callback) {
                handlers.length = 0;
              } else {
                // Unbind specific ones
                hi = handlers.length;
                while (hi--) {
                  if (handlers[hi].func === callback) {
                    handlers = handlers.slice(0, hi).concat(handlers.slice(hi + 1));
                    bindings[name] = handlers;
                  }
                }
              }

              if (!handlers.length) {
                toggleEvent(name, false);
                delete bindings[name];
              }
            }
          }
        } else {
          for (name in bindings) {
            toggleEvent(name, false);
          }

          bindings = {};
        }

        return self;
      };

      /**
       * Binds an event listener to a specific event by name
       * and automatically unbind the event once the callback fires.
       *
       * @method once
       * @param {String} name Event name or space separated list of events to bind.
       * @param {callback} callback Callback to be executed when the event occurs.
       * @param {Boolean} first Optional flag if the event should be prepended. Use this with care.
       * @return {Object} Current class instance.
       * @example
       * instance.once('event', function(e) {
       *     // Callback logic
       * });
       */
      var once = function (name, callback, prepend) {
        return on(name, callback, prepend, { once: true });
      };

      /**
       * Returns true/false if the dispatcher has a event of the specified name.
       *
       * @method has
       * @param {String} name Name of the event to check for.
       * @return {Boolean} true/false if the event exists or not.
       */
      var has = function (name) {
        name = name.toLowerCase();
        return !(!bindings[name] || bindings[name].length === 0);
      };

      // Expose
      self.fire = fire;
      self.on = on;
      self.off = off;
      self.once = once;
      self.has = has;
    };

    /**
     * Returns true/false if the specified event name is a native browser event or not.
     *
     * @method isNative
     * @param {String} name Name to check if it's native.
     * @return {Boolean} true/false if the event is native or not.
     * @static
     */
    Dispatcher.isNative = function (name) {
      return !!nativeEvents[name.toLowerCase()];
    };

    return Dispatcher;
  }
);

/**
 * Observable.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
 */

/**
 * This mixin will add event binding logic to classes.
 *
 * @mixin tinymce.util.Observable
 */
define(
  'tinymce.core.util.Observable',
  [
    "tinymce.core.util.EventDispatcher"
  ],
  function (EventDispatcher) {
    var getEventDispatcher = function (obj) {
      if (!obj._eventDispatcher) {
        obj._eventDispatcher = new EventDispatcher({
          scope: obj,
          toggleEvent: function (name, state) {
            if (EventDispatcher.isNative(name) && obj.toggleNativeEvent) {
              obj.toggleNativeEvent(name, state);
            }
          }
        });
      }

      return obj._eventDispatcher;
    };

    return {
      /**
       * Fires the specified event by name. Consult the
       * <a href="/docs/advanced/events">event reference</a> for more details on each event.
       *
       * @method fire
       * @param {String} name Name of the event to fire.
       * @param {Object?} args Event arguments.
       * @param {Boolean?} bubble True/false if the event is to be bubbled.
       * @return {Object} Event args instance passed in.
       * @example
       * instance.fire('event', {...});
       */
      fire: function (name, args, bubble) {
        var self = this;

        // Prevent all events except the remove event after the instance has been removed
        if (self.removed && name !== "remove") {
          return args;
        }

        args = getEventDispatcher(self).fire(name, args, bubble);

        // Bubble event up to parents
        if (bubble !== false && self.parent) {
          var parent = self.parent();
          while (parent && !args.isPropagationStopped()) {
            parent.fire(name, args, false);
            parent = parent.parent();
          }
        }

        return args;
      },

      /**
       * Binds an event listener to a specific event by name. Consult the
       * <a href="/docs/advanced/events">event reference</a> for more details on each event.
       *
       * @method on
       * @param {String} name Event name or space separated list of events to bind.
       * @param {callback} callback Callback to be executed when the event occurs.
       * @param {Boolean} first Optional flag if the event should be prepended. Use this with care.
       * @return {Object} Current class instance.
       * @example
       * instance.on('event', function(e) {
       *     // Callback logic
       * });
       */
      on: function (name, callback, prepend) {
        return getEventDispatcher(this).on(name, callback, prepend);
      },

      /**
       * Unbinds an event listener to a specific event by name. Consult the
       * <a href="/docs/advanced/events">event reference</a> for more details on each event.
       *
       * @method off
       * @param {String?} name Name of the event to unbind.
       * @param {callback?} callback Callback to unbind.
       * @return {Object} Current class instance.
       * @example
       * // Unbind specific callback
       * instance.off('event', handler);
       *
       * // Unbind all listeners by name
       * instance.off('event');
       *
       * // Unbind all events
       * instance.off();
       */
      off: function (name, callback) {
        return getEventDispatcher(this).off(name, callback);
      },

      /**
       * Bind the event callback and once it fires the callback is removed. Consult the
       * <a href="/docs/advanced/events">event reference</a> for more details on each event.
       *
       * @method once
       * @param {String} name Name of the event to bind.
       * @param {callback} callback Callback to bind only once.
       * @return {Object} Current class instance.
       */
      once: function (name, callback) {
        return getEventDispatcher(this).once(name, callback);
      },

      /**
       * Returns true/false if the object has a event of the specified name.
       *
       * @method hasEventListeners
       * @param {String} name Name of the event to check for.
       * @return {Boolean} true/false if the event exists or not.
       */
      hasEventListeners: function (name) {
        return getEventDispatcher(this).has(name);
      }
    };
  }
);
/**
 * EditorObservable.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
 */

/**
 * This mixin contains the event logic for the tinymce.Editor class.
 *
 * @mixin tinymce.EditorObservable
 * @extends tinymce.util.Observable
 */
define(
  'tinymce.core.EditorObservable',
  [
    "tinymce.core.util.Observable",
    "tinymce.core.dom.DOMUtils",
    "tinymce.core.util.Tools"
  ],
  function (Observable, DOMUtils, Tools) {
    var DOM = DOMUtils.DOM, customEventRootDelegates;

    /**
     * Returns the event target so for the specified event. Some events fire
     * only on document, some fire on documentElement etc. This also handles the
     * custom event root setting where it returns that element instead of the body.
     *
     * @private
     * @param {tinymce.Editor} editor Editor instance to get event target from.
     * @param {String} eventName Name of the event for example "click".
     * @return {Element/Document} HTML Element or document target to bind on.
     */
    var getEventTarget = function (editor, eventName) {
      if (eventName == 'selectionchange') {
        return editor.getDoc();
      }

      // Need to bind mousedown/mouseup etc to document not body in iframe mode
      // Since the user might click on the HTML element not the BODY
      if (!editor.inline && /^mouse|touch|click|contextmenu|drop|dragover|dragend/.test(eventName)) {
        return editor.getDoc().documentElement;
      }

      // Bind to event root instead of body if it's defined
      if (editor.settings.event_root) {
        if (!editor.eventRoot) {
          editor.eventRoot = DOM.select(editor.settings.event_root)[0];
        }

        return editor.eventRoot;
      }

      return editor.getBody();
    };

    /**
     * Binds a event delegate for the specified name this delegate will fire
     * the event to the editor dispatcher.
     *
     * @private
     * @param {tinymce.Editor} editor Editor instance to get event target from.
     * @param {String} eventName Name of the event for example "click".
     */
    var bindEventDelegate = function (editor, eventName) {
      var eventRootElm, delegate;

      var isListening = function (editor) {
        return !editor.hidden && !editor.readonly;
      };

      if (!editor.delegates) {
        editor.delegates = {};
      }

      if (editor.delegates[eventName] || editor.removed) {
        return;
      }

      eventRootElm = getEventTarget(editor, eventName);

      if (editor.settings.event_root) {
        if (!customEventRootDelegates) {
          customEventRootDelegates = {};
          editor.editorManager.on('removeEditor', function () {
            var name;

            if (!editor.editorManager.activeEditor) {
              if (customEventRootDelegates) {
                for (name in customEventRootDelegates) {
                  editor.dom.unbind(getEventTarget(editor, name));
                }

                customEventRootDelegates = null;
              }
            }
          });
        }

        if (customEventRootDelegates[eventName]) {
          return;
        }

        delegate = function (e) {
          var target = e.target, editors = editor.editorManager.get(), i = editors.length;

          while (i--) {
            var body = editors[i].getBody();

            if (body === target || DOM.isChildOf(target, body)) {
              if (isListening(editors[i])) {
                editors[i].fire(eventName, e);
              }
            }
          }
        };

        customEventRootDelegates[eventName] = delegate;
        DOM.bind(eventRootElm, eventName, delegate);
      } else {
        delegate = function (e) {
          if (isListening(editor)) {
            editor.fire(eventName, e);
          }
        };

        DOM.bind(eventRootElm, eventName, delegate);
        editor.delegates[eventName] = delegate;
      }
    };

    var EditorObservable = {
      /**
       * Bind any pending event delegates. This gets executed after the target body/document is created.
       *
       * @private
       */
      bindPendingEventDelegates: function () {
        var self = this;

        Tools.each(self._pendingNativeEvents, function (name) {
          bindEventDelegate(self, name);
        });
      },

      /**
       * Toggles a native event on/off this is called by the EventDispatcher when
       * the first native event handler is added and when the last native event handler is removed.
       *
       * @private
       */
      toggleNativeEvent: function (name, state) {
        var self = this;

        // Never bind focus/blur since the FocusManager fakes those
        if (name == "focus" || name == "blur") {
          return;
        }

        if (state) {
          if (self.initialized) {
            bindEventDelegate(self, name);
          } else {
            if (!self._pendingNativeEvents) {
              self._pendingNativeEvents = [name];
            } else {
              self._pendingNativeEvents.push(name);
            }
          }
        } else if (self.initialized) {
          self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]);
          delete self.delegates[name];
        }
      },

      /**
       * Unbinds all native event handlers that means delegates, custom events bound using the Events API etc.
       *
       * @private
       */
      unbindAllNativeEvents: function() {
          var self = this, name;

          if (self.delegates) {
              for (name in self.delegates) {
                  self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]);
              }

              delete self.delegates;
          }

          if (!self.inline) {
              if (self.getBody() != null && self.getBody().hasOwnProperty('onload'))
                  self.getBody().onload = null;
              if (self.hasOwnProperty('getWin'))
                  self.dom.unbind(self.getWin());
              if (self.hasOwnProperty('getDoc'))
                  self.dom.unbind(self.getDoc());
          }

          if (self.hasOwnProperty('getBody'))
              self.dom.unbind(self.getBody());
          if (self.hasOwnProperty('getContainer'))
              self.dom.unbind(self.getContainer());
      }
    };

    EditorObservable = Tools.extend({}, Observable, EditorObservable);

    return EditorObservable;
  }
);

/**
 * ErrorReporter.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
 */

/**
 * Various error reporting helper functions.
 *
 * @class tinymce.ErrorReporter
 * @private
 */
define(
  'tinymce.core.ErrorReporter',
  [
    'global!window',
    'tinymce.core.AddOnManager'
  ],
  function (window, AddOnManager) {
    var PluginManager = AddOnManager.PluginManager;

    var resolvePluginName = function (targetUrl, suffix) {
      for (var name in PluginManager.urls) {
        var matchUrl = PluginManager.urls[name] + '/plugin' + suffix + '.js';
        if (matchUrl === targetUrl) {
          return name;
        }
      }

      return null;
    };

    var pluginUrlToMessage = function (editor, url) {
      var plugin = resolvePluginName(url, editor.suffix);
      return plugin ?
        'Failed to load plugin: ' + plugin + ' from url ' + url :
        'Failed to load plugin url: ' + url;
    };

    var displayNotification = function (editor, message) {
      editor.notificationManager.open({
        type: 'error',
        text: message
      });
    };

    var displayError = function (editor, message) {
      if (editor._skinLoaded) {
        displayNotification(editor, message);
      } else {
        editor.on('SkinLoaded', function () {
          displayNotification(editor, message);
        });
      }
    };

    var uploadError = function (editor, message) {
      displayError(editor, 'Failed to upload image: ' + message);
    };

    var pluginLoadError = function (editor, url) {
      displayError(editor, pluginUrlToMessage(editor, url));
    };

    var initError = function (message) {
      var console = window.console;
      if (console && !window.test) { // Skip test env
        if (console.error) {
          console.error.apply(console, arguments);
        } else {
          console.log.apply(console, arguments);
        }
      }
    };

    return {
      pluginLoadError: pluginLoadError,
      uploadError: uploadError,
      displayError: displayError,
      initError: initError
    };
  }
);
/**
 * CaretContainerInput.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
 */

/**
 * This module shows the invisble block that the caret is currently in when contents is added to that block.
 */
define(
  'tinymce.core.caret.CaretContainerInput',
  [
    'ephox.katamari.api.Fun',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.search.SelectorFind',
    'tinymce.core.caret.CaretContainer'
  ],
  function (Fun, Element, SelectorFind, CaretContainer) {
    var findBlockCaretContainer = function (editor) {
      return SelectorFind.descendant(Element.fromDom(editor.getBody()), '*[data-mce-caret]').fold(Fun.constant(null), function (elm) {
        return elm.dom();
      });
    };

    var removeIeControlRect = function (editor) {
      editor.selection.setRng(editor.selection.getRng());
    };

    var showBlockCaretContainer = function (editor, blockCaretContainer) {
      if (blockCaretContainer.hasAttribute('data-mce-caret')) {
        CaretContainer.showCaretContainerBlock(blockCaretContainer);
        removeIeControlRect(editor);
        editor.selection.scrollIntoView(blockCaretContainer);
      }
    };

    var handleBlockContainer = function (editor, e) {
      var blockCaretContainer = findBlockCaretContainer(editor);

      if (!blockCaretContainer) {
        return;
      }

      if (e.type === 'compositionstart') {
        e.preventDefault();
        e.stopPropagation();
        showBlockCaretContainer(blockCaretContainer);
        return;
      }

      if (CaretContainer.hasContent(blockCaretContainer)) {
        showBlockCaretContainer(editor, blockCaretContainer);
      }
    };

    var setup = function (editor) {
      editor.on('keyup compositionstart', Fun.curry(handleBlockContainer, editor));
    };

    return {
      setup: setup
    };
  }
);
define(
  'ephox.sand.api.XMLHttpRequest',

  [
    'ephox.sand.util.Global'
  ],

  function (Global) {
    /*
     * IE8 and above per
     * https://developer.mozilla.org/en/docs/XMLHttpRequest
     */
    return function () {
      var f = Global.getOrDie('XMLHttpRequest');
      return new f();
    };
  }
);
/**
 * Uploader.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
 */

/**
 * Upload blobs or blob infos to the specified URL or handler.
 *
 * @private
 * @class tinymce.file.Uploader
 * @example
 * var uploader = new Uploader({
 *     url: '/upload.php',
 *     basePath: '/base/path',
 *     credentials: true,
 *     handler: function(data, success, failure) {
 *         ...
 *     }
 * });
 *
 * uploader.upload(blobInfos).then(function(result) {
 *     ...
 * });
 */
define(
  'tinymce.core.file.Uploader',
  [
    'ephox.sand.api.XMLHttpRequest',
    'global!window',
    'tinymce.core.util.Fun',
    'tinymce.core.util.Promise',
    'tinymce.core.util.Tools'
  ],
  function (XMLHttpRequest, window, Fun, Promise, Tools) {
    return function (uploadStatus, settings) {
      var pendingPromises = {};

      var pathJoin = function (path1, path2) {
        if (path1) {
          return path1.replace(/\/$/, '') + '/' + path2.replace(/^\//, '');
        }

        return path2;
      };

      var defaultHandler = function (blobInfo, success, failure, progress) {
        var xhr, formData;

        xhr = new XMLHttpRequest();
        xhr.open('POST', settings.url);
        xhr.withCredentials = settings.credentials;

        xhr.upload.onprogress = function (e) {
          progress(e.loaded / e.total * 100);
        };

        xhr.onerror = function () {
          failure("Image upload failed due to a XHR Transport error. Code: " + xhr.status);
        };

        xhr.onload = function () {
          var json;

          if (xhr.status < 200 || xhr.status >= 300) {
            failure("HTTP Error: " + xhr.status);
            return;
          }

          json = JSON.parse(xhr.responseText);

          if (!json || typeof json.location != "string") {
            failure("Invalid JSON: " + xhr.responseText);
            return;
          }

          success(pathJoin(settings.basePath, json.location));
        };

        formData = new window.FormData(); // TODO: Stick this in sand
        formData.append('file', blobInfo.blob(), blobInfo.filename());

        xhr.send(formData);
      };

      var noUpload = function () {
        return new Promise(function (resolve) {
          resolve([]);
        });
      };

      var handlerSuccess = function (blobInfo, url) {
        return {
          url: url,
          blobInfo: blobInfo,
          status: true
        };
      };

      var handlerFailure = function (blobInfo, error) {
        return {
          url: '',
          blobInfo: blobInfo,
          status: false,
          error: error
        };
      };

      var resolvePending = function (blobUri, result) {
        Tools.each(pendingPromises[blobUri], function (resolve) {
          resolve(result);
        });

        delete pendingPromises[blobUri];
      };

      var uploadBlobInfo = function (blobInfo, handler, openNotification) {
        uploadStatus.markPending(blobInfo.blobUri());

        return new Promise(function (resolve) {
          var notification, progress;

          var noop = function () {
          };

          try {
            var closeNotification = function () {
              if (notification) {
                notification.close();
                progress = noop; // Once it's closed it's closed
              }
            };

            var success = function (url) {
              closeNotification();
              uploadStatus.markUploaded(blobInfo.blobUri(), url);
              resolvePending(blobInfo.blobUri(), handlerSuccess(blobInfo, url));
              resolve(handlerSuccess(blobInfo, url));
            };

            var failure = function (error) {
              closeNotification();
              uploadStatus.removeFailed(blobInfo.blobUri());
              resolvePending(blobInfo.blobUri(), handlerFailure(blobInfo, error));
              resolve(handlerFailure(blobInfo, error));
            };

            progress = function (percent) {
              if (percent < 0 || percent > 100) {
                return;
              }

              if (!notification) {
                notification = openNotification();
              }

              notification.progressBar.value(percent);
            };

            handler(blobInfo, success, failure, progress);
          } catch (ex) {
            resolve(handlerFailure(blobInfo, ex.message));
          }
        });
      };

      var isDefaultHandler = function (handler) {
        return handler === defaultHandler;
      };

      var pendingUploadBlobInfo = function (blobInfo) {
        var blobUri = blobInfo.blobUri();

        return new Promise(function (resolve) {
          pendingPromises[blobUri] = pendingPromises[blobUri] || [];
          pendingPromises[blobUri].push(resolve);
        });
      };

      var uploadBlobs = function (blobInfos, openNotification) {
        blobInfos = Tools.grep(blobInfos, function (blobInfo) {
          return !uploadStatus.isUploaded(blobInfo.blobUri());
        });

        return Promise.all(Tools.map(blobInfos, function (blobInfo) {
          return uploadStatus.isPending(blobInfo.blobUri()) ?
            pendingUploadBlobInfo(blobInfo) : uploadBlobInfo(blobInfo, settings.handler, openNotification);
        }));
      };

      var upload = function (blobInfos, openNotification) {
        return (!settings.url && isDefaultHandler(settings.handler)) ? noUpload() : uploadBlobs(blobInfos, openNotification);
      };

      settings = Tools.extend({
        credentials: false,
        // We are adding a notify argument to this (at the moment, until it doesn't work)
        handler: defaultHandler
      }, settings);

      return {
        upload: upload
      };
    };
  }
);
define(
  'ephox.sand.api.Blob',

  [
    'ephox.sand.util.Global'
  ],

  function (Global) {
    /*
     * IE10 and above per
     * https://developer.mozilla.org/en-US/docs/Web/API/Blob
     */
    return function (parts, properties) {
      var f = Global.getOrDie('Blob');
      return new f(parts, properties);
    };
  }
);
define(
  'ephox.sand.api.FileReader',

  [
    'ephox.sand.util.Global'
  ],

  function (Global) {
    /*
     * IE10 and above per
     * https://developer.mozilla.org/en-US/docs/Web/API/FileReader
     */
    return function () {
      var f = Global.getOrDie('FileReader');
      return new f();
    };
  }
);
define(
  'ephox.sand.api.Uint8Array',

  [
    'ephox.sand.util.Global'
  ],

  function (Global) {
    /*
     * https://developer.mozilla.org/en-US/docs/Web/API/Uint8Array
     *
     * IE10 and above per
     * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays
     */
    return function (arr) {
      var f = Global.getOrDie('Uint8Array');
      return new f(arr);
    };
  }
);
define(
  'ephox.sand.api.Window',

  [
    'ephox.sand.util.Global'
  ],

  function (Global) {
    /******************************************************************************************
     * BIG BIG WARNING: Don't put anything other than top-level window functions in here.
     *
     * Objects that are technically available as window.X should be in their own module X (e.g. Blob, FileReader, URL).
     ******************************************************************************************
     */

    /*
     * IE10 and above per
     * https://developer.mozilla.org/en/docs/Web/API/window.requestAnimationFrame
     */
    var requestAnimationFrame = function (callback) {
      var f = Global.getOrDie('requestAnimationFrame');
      f(callback);
    };

    /*
     * IE10 and above per
     * https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64.atob
     */
    var atob = function (base64) {
      var f = Global.getOrDie('atob');
      return f(base64);
    };

    return {
      atob: atob,
      requestAnimationFrame: requestAnimationFrame
    };
  }
);
/**
 * Conversions.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
 */

/**
 * Converts blob/uris back and forth.
 *
 * @private
 * @class tinymce.file.Conversions
 */
define(
  'tinymce.core.file.Conversions',
  [
    'ephox.sand.api.Blob',
    'ephox.sand.api.FileReader',
    'ephox.sand.api.Uint8Array',
    'ephox.sand.api.Window',
    'ephox.sand.api.XMLHttpRequest',
    'tinymce.core.util.Promise'
  ],
  function (Blob, FileReader, Uint8Array, Window, XMLHttpRequest, Promise) {
    var blobUriToBlob = function (url) {
      return new Promise(function (resolve, reject) {

        var rejectWithError = function () {
          reject("Cannot convert " + url + " to Blob. Resource might not exist or is inaccessible.");
        };

        try {
          var xhr = new XMLHttpRequest();

          xhr.open('GET', url, true);
          xhr.responseType = 'blob';

          xhr.onload = function () {
            if (this.status == 200) {
              resolve(this.response);
            } else {
              // IE11 makes it into onload but responds with status 500
              rejectWithError();
            }
          };

          // Chrome fires an error event instead of the exception
          // Also there seems to be no way to intercept the message that is logged to the console
          xhr.onerror = rejectWithError;

          xhr.send();
        } catch (ex) {
          rejectWithError();
        }
      });
    };

    var parseDataUri = function (uri) {
      var type, matches;

      uri = decodeURIComponent(uri).split(',');

      matches = /data:([^;]+)/.exec(uri[0]);
      if (matches) {
        type = matches[1];
      }

      return {
        type: type,
        data: uri[1]
      };
    };

    var dataUriToBlob = function (uri) {
      return new Promise(function (resolve) {
        var str, arr, i;

        uri = parseDataUri(uri);

        // Might throw error if data isn't proper base64
        try {
          str = Window.atob(uri.data);
        } catch (e) {
          resolve(new Blob([]));
          return;
        }

        arr = new Uint8Array(str.length);

        for (i = 0; i < arr.length; i++) {
          arr[i] = str.charCodeAt(i);
        }

        resolve(new Blob([arr], { type: uri.type }));
      });
    };

    var uriToBlob = function (url) {
      if (url.indexOf('blob:') === 0) {
        return blobUriToBlob(url);
      }

      if (url.indexOf('data:') === 0) {
        return dataUriToBlob(url);
      }

      return null;
    };

    var blobToDataUri = function (blob) {
      return new Promise(function (resolve) {
        var reader = new FileReader();

        reader.onloadend = function () {
          resolve(reader.result);
        };

        reader.readAsDataURL(blob);
      });
    };

    return {
      uriToBlob: uriToBlob,
      blobToDataUri: blobToDataUri,
      parseDataUri: parseDataUri
    };
  }
);
/**
 * ImageScanner.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
 */

/**
 * Finds images with data uris or blob uris. If data uris are found it will convert them into blob uris.
 *
 * @private
 * @class tinymce.file.ImageScanner
 */
define(
  'tinymce.core.file.ImageScanner',
  [
    "tinymce.core.util.Promise",
    "tinymce.core.util.Arr",
    "tinymce.core.util.Fun",
    "tinymce.core.file.Conversions",
    "tinymce.core.Env"
  ],
  function (Promise, Arr, Fun, Conversions, Env) {
    var count = 0;

    var uniqueId = function (prefix) {
      return (prefix || 'blobid') + (count++);
    };

    var imageToBlobInfo = function (blobCache, img, resolve, reject) {
      var base64, blobInfo;

      if (img.src.indexOf('blob:') === 0) {
        blobInfo = blobCache.getByUri(img.src);

        if (blobInfo) {
          resolve({
            image: img,
            blobInfo: blobInfo
          });
        } else {
          Conversions.uriToBlob(img.src).then(function (blob) {
            Conversions.blobToDataUri(blob).then(function (dataUri) {
              base64 = Conversions.parseDataUri(dataUri).data;
              blobInfo = blobCache.create(uniqueId(), blob, base64);
              blobCache.add(blobInfo);

              resolve({
                image: img,
                blobInfo: blobInfo
              });
            });
          }, function (err) {
            reject(err);
          });
        }

        return;
      }

      base64 = Conversions.parseDataUri(img.src).data;
      blobInfo = blobCache.findFirst(function (cachedBlobInfo) {
        return cachedBlobInfo.base64() === base64;
      });

      if (blobInfo) {
        resolve({
          image: img,
          blobInfo: blobInfo
        });
      } else {
        Conversions.uriToBlob(img.src).then(function (blob) {
          blobInfo = blobCache.create(uniqueId(), blob, base64);
          blobCache.add(blobInfo);

          resolve({
            image: img,
            blobInfo: blobInfo
          });
        }, function (err) {
          reject(err);
        });
      }
    };

    var getAllImages = function (elm) {
      return elm ? elm.getElementsByTagName('img') : [];
    };

    return function (uploadStatus, blobCache) {
      var cachedPromises = {};

      var findAll = function (elm, predicate) {
        var images, promises;

        if (!predicate) {
          predicate = Fun.constant(true);
        }

        images = Arr.filter(getAllImages(elm), function (img) {
          var src = img.src;

          if (!Env.fileApi) {
            return false;
          }

          if (img.hasAttribute('data-mce-bogus')) {
            return false;
          }

          if (img.hasAttribute('data-mce-placeholder')) {
            return false;
          }

          if (!src || src == Env.transparentSrc) {
            return false;
          }

          if (src.indexOf('blob:') === 0) {
            return !uploadStatus.isUploaded(src);
          }

          if (src.indexOf('data:') === 0) {
            return predicate(img);
          }

          return false;
        });

        promises = Arr.map(images, function (img) {
          var newPromise;

          if (cachedPromises[img.src]) {
            // Since the cached promise will return the cached image
            // We need to wrap it and resolve with the actual image
            return new Promise(function (resolve) {
              cachedPromises[img.src].then(function (imageInfo) {
                if (typeof imageInfo === 'string') { // error apparently
                  return imageInfo;
                }
                resolve({
                  image: img,
                  blobInfo: imageInfo.blobInfo
                });
              });
            });
          }

          newPromise = new Promise(function (resolve, reject) {
            imageToBlobInfo(blobCache, img, resolve, reject);
          }).then(function (result) {
            delete cachedPromises[result.image.src];
            return result;
          })['catch'](function (error) {
            delete cachedPromises[img.src];
            return error;
          });

          cachedPromises[img.src] = newPromise;

          return newPromise;
        });

        return Promise.all(promises);
      };

      return {
        findAll: findAll
      };
    };
  }
);
/**
 * Uuid.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
 */

/**
 * Generates unique ids.
 *
 * @class tinymce.util.Uuid
 * @private
 */
define(
  'tinymce.core.util.Uuid',
  [
  ],
  function () {
    var count = 0;

    var seed = function () {
      var rnd = function () {
        return Math.round(Math.random() * 0xFFFFFFFF).toString(36);
      };

      var now = new Date().getTime();
      return 's' + now.toString(36) + rnd() + rnd() + rnd();
    };

    var uuid = function (prefix) {
      return prefix + (count++) + seed();
    };

    return {
      uuid: uuid
    };
  }
);

/**
 * BlobCache.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
 */

/**
 * Hold blob info objects where a blob has extra internal information.
 *
 * @private
 * @class tinymce.file.BlobCache
 */
define(
  'tinymce.core.file.BlobCache',
  [
    'ephox.sand.api.URL',
    'tinymce.core.util.Arr',
    'tinymce.core.util.Fun',
    'tinymce.core.util.Uuid'
  ],
  function (URL, Arr, Fun, Uuid) {
    return function () {
      var cache = [], constant = Fun.constant;

      var mimeToExt = function (mime) {
        var mimes = {
          'image/jpeg': 'jpg',
          'image/jpg': 'jpg',
          'image/gif': 'gif',
          'image/png': 'png'
        };

        return mimes[mime.toLowerCase()] || 'dat';
      };

      var create = function (o, blob, base64, filename) {
        return typeof o === 'object' ? toBlobInfo(o) : toBlobInfo({
          id: o,
          name: filename,
          blob: blob,
          base64: base64
        });
      };

      var toBlobInfo = function (o) {
        var id, name;

        if (!o.blob || !o.base64) {
          throw "blob and base64 representations of the image are required for BlobInfo to be created";
        }

        id = o.id || Uuid.uuid('blobid');
        name = o.name || id;

        return {
          id: constant(id),
          name: constant(name),
          filename: constant(name + '.' + mimeToExt(o.blob.type)),
          blob: constant(o.blob),
          base64: constant(o.base64),
          blobUri: constant(o.blobUri || URL.createObjectURL(o.blob)),
          uri: constant(o.uri)
        };
      };

      var add = function (blobInfo) {
        if (!get(blobInfo.id())) {
          cache.push(blobInfo);
        }
      };

      var get = function (id) {
        return findFirst(function (cachedBlobInfo) {
          return cachedBlobInfo.id() === id;
        });
      };

      var findFirst = function (predicate) {
        return Arr.filter(cache, predicate)[0];
      };

      var getByUri = function (blobUri) {
        return findFirst(function (blobInfo) {
          return blobInfo.blobUri() == blobUri;
        });
      };

      var removeByUri = function (blobUri) {
        cache = Arr.filter(cache, function (blobInfo) {
          if (blobInfo.blobUri() === blobUri) {
            URL.revokeObjectURL(blobInfo.blobUri());
            return false;
          }

          return true;
        });
      };

      var destroy = function () {
        Arr.each(cache, function (cachedBlobInfo) {
          URL.revokeObjectURL(cachedBlobInfo.blobUri());
        });

        cache = [];
      };

      return {
        create: create,
        add: add,
        get: get,
        getByUri: getByUri,
        findFirst: findFirst,
        removeByUri: removeByUri,
        destroy: destroy
      };
    };
  }
);
/**
 * UploadStatus.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
 */

/**
 * Holds the current status of a blob uri, if it's pending or uploaded and what the result urls was.
 *
 * @private
 * @class tinymce.file.UploadStatus
 */
define(
  'tinymce.core.file.UploadStatus',
  [
  ],
  function () {
    return function () {
      var PENDING = 1, UPLOADED = 2;
      var blobUriStatuses = {};

      var createStatus = function (status, resultUri) {
        return {
          status: status,
          resultUri: resultUri
        };
      };

      var hasBlobUri = function (blobUri) {
        return blobUri in blobUriStatuses;
      };

      var getResultUri = function (blobUri) {
        var result = blobUriStatuses[blobUri];

        return result ? result.resultUri : null;
      };

      var isPending = function (blobUri) {
        return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === PENDING : false;
      };

      var isUploaded = function (blobUri) {
        return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === UPLOADED : false;
      };

      var markPending = function (blobUri) {
        blobUriStatuses[blobUri] = createStatus(PENDING, null);
      };

      var markUploaded = function (blobUri, resultUri) {
        blobUriStatuses[blobUri] = createStatus(UPLOADED, resultUri);
      };

      var removeFailed = function (blobUri) {
        delete blobUriStatuses[blobUri];
      };

      var destroy = function () {
        blobUriStatuses = {};
      };

      return {
        hasBlobUri: hasBlobUri,
        getResultUri: getResultUri,
        isPending: isPending,
        isUploaded: isUploaded,
        markPending: markPending,
        markUploaded: markUploaded,
        removeFailed: removeFailed,
        destroy: destroy
      };
    };
  }
);
/**
 * EditorUpload.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 image uploads, updates undo stack and patches over various internal functions.
 *
 * @private
 * @class tinymce.EditorUpload
 */
define(
  'tinymce.core.EditorUpload',
  [
    "tinymce.core.util.Arr",
    "tinymce.core.file.Uploader",
    "tinymce.core.file.ImageScanner",
    "tinymce.core.file.BlobCache",
    "tinymce.core.file.UploadStatus",
    "tinymce.core.ErrorReporter"
  ],
  function (Arr, Uploader, ImageScanner, BlobCache, UploadStatus, ErrorReporter) {
    return function (editor) {
      var blobCache = new BlobCache(), uploader, imageScanner, settings = editor.settings;
      var uploadStatus = new UploadStatus();

      var aliveGuard = function (callback) {
        return function (result) {
          if (editor.selection) {
            return callback(result);
          }

          return [];
        };
      };

      var cacheInvalidator = function () {
        return '?' + (new Date()).getTime();
      };

      // Replaces strings without regexps to avoid FF regexp to big issue
      var replaceString = function (content, search, replace) {
        var index = 0;

        do {
          index = content.indexOf(search, index);

          if (index !== -1) {
            content = content.substring(0, index) + replace + content.substr(index + search.length);
            index += replace.length - search.length + 1;
          }
        } while (index !== -1);

        return content;
      };

      var replaceImageUrl = function (content, targetUrl, replacementUrl) {
        content = replaceString(content, 'src="' + targetUrl + '"', 'src="' + replacementUrl + '"');
        content = replaceString(content, 'data-mce-src="' + targetUrl + '"', 'data-mce-src="' + replacementUrl + '"');

        return content;
      };

      var replaceUrlInUndoStack = function (targetUrl, replacementUrl) {
        Arr.each(editor.undoManager.data, function (level) {
          if (level.type === 'fragmented') {
            level.fragments = Arr.map(level.fragments, function (fragment) {
              return replaceImageUrl(fragment, targetUrl, replacementUrl);
            });
          } else {
            level.content = replaceImageUrl(level.content, targetUrl, replacementUrl);
          }
        });
      };

      var openNotification = function () {
        return editor.notificationManager.open({
          text: editor.translate('Image uploading...'),
          type: 'info',
          timeout: -1,
          progressBar: true
        });
      };

      var replaceImageUri = function (image, resultUri) {
          blobCache.removeByUri(image.src);
          replaceUrlInUndoStack(image.src, resultUri);

          editor.$(image).attr({
              src: settings.images_reuse_filename ? resultUri + cacheInvalidator() : resultUri,
              'data-mce-src': editor.convertURL(resultUri, 'src')
          });
      };

      var uploadImages = function (callback) {
        if (!uploader) {
          uploader = new Uploader(uploadStatus, {
            url: settings.images_upload_url,
            basePath: settings.images_upload_base_path,
            credentials: settings.images_upload_credentials,
            handler: settings.images_upload_handler
          });
        }

        return scanForImages().then(aliveGuard(function (imageInfos) {
          var blobInfos;

          blobInfos = Arr.map(imageInfos, function (imageInfo) {
            return imageInfo.blobInfo;
          });

          return uploader.upload(blobInfos, openNotification).then(aliveGuard(function (result) {
            var filteredResult = Arr.map(result, function (uploadInfo, index) {
              var image = imageInfos[index].image;
              if (uploadInfo.status && editor.settings.images_replace_blob_uris !== false) {
                replaceImageUri(image, uploadInfo.url);
              } else if (uploadInfo.error) {
                ErrorReporter.uploadError(editor, uploadInfo.error);
              }

              return {
                element: image,
                status: uploadInfo.status
              };
            });

            if (callback) {
              callback(filteredResult);
            }

            return filteredResult;
          }));
        }));
      };

      var uploadImagesAuto = function (callback) {
        if (settings.automatic_uploads !== false) {
          return uploadImages(callback);
        }
      };

      var isValidDataUriImage = function (imgElm) {
        return settings.images_dataimg_filter ? settings.images_dataimg_filter(imgElm) : true;
      };

      var scanForImages = function () {
        if (!imageScanner) {
          imageScanner = new ImageScanner(uploadStatus, blobCache);
        }

        return imageScanner.findAll(editor.getBody(), isValidDataUriImage).then(aliveGuard(function (result) {
          result = Arr.filter(result, function (resultItem) {
            // ImageScanner internally converts images that it finds, but it may fail to do so if image source is inaccessible.
            // In such case resultItem will contain appropriate text error message, instead of image data.
            if (typeof resultItem === 'string') {
              ErrorReporter.displayError(editor, resultItem);
              return false;
            }
            return true;
          });

          Arr.each(result, function (resultItem) {
            replaceUrlInUndoStack(resultItem.image.src, resultItem.blobInfo.blobUri());
            resultItem.image.src = resultItem.blobInfo.blobUri();
            resultItem.image.removeAttribute('data-mce-src');
          });

          return result;
        }));
      };

      var destroy = function () {
        blobCache.destroy();
        uploadStatus.destroy();
        imageScanner = uploader = null;
      };

      var replaceBlobUris = function (content) {
        return content.replace(/src="(blob:[^"]+)"/g, function (match, blobUri) {
          var resultUri = uploadStatus.getResultUri(blobUri);

          if (resultUri) {
            return 'src="' + resultUri + '"';
          }

          var blobInfo = blobCache.getByUri(blobUri);

          if (!blobInfo) {
            blobInfo = Arr.reduce(editor.editorManager.get(), function (result, editor) {
              return result || editor.editorUpload && editor.editorUpload.blobCache.getByUri(blobUri);
            }, null);
          }

          if (blobInfo) {
            return 'src="data:' + blobInfo.blob().type + ';base64,' + blobInfo.base64() + '"';
          }

          return match;
        });
      };

      editor.on('setContent', function () {
        if (editor.settings.automatic_uploads !== false) {
          uploadImagesAuto();
        } else {
          scanForImages();
        }
      });

      editor.on('RawSaveContent', function (e) {
        e.content = replaceBlobUris(e.content);
      });

      editor.on('getContent', function (e) {
        if (e.source_view || e.format == 'raw') {
          return;
        }

        e.content = replaceBlobUris(e.content);
      });

      editor.on('PostRender', function () {
        editor.parser.addNodeFilter('img', function (images) {
          Arr.each(images, function (img) {
            var src = img.attr('src');

            if (blobCache.getByUri(src)) {
              return;
            }

            var resultUri = uploadStatus.getResultUri(src);
            if (resultUri) {
              img.attr('src', resultUri);
            }
          });
        });
      });

      return {
        blobCache: blobCache,
        uploadImages: uploadImages,
        uploadImagesAuto: uploadImagesAuto,
        scanForImages: scanForImages,
        destroy: destroy
      };
    };
  }
);
/**
 * ForceBlocks.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
 */

/**
 * Makes sure that everything gets wrapped in paragraphs.
 *
 * @private
 * @class tinymce.ForceBlocks
 */
define(
  'tinymce.core.ForceBlocks',
  [
    'ephox.katamari.api.Fun'
  ],
  function (Fun) {
    var addRootBlocks = function (editor) {
      var settings = editor.settings, dom = editor.dom, selection = editor.selection;
      var schema = editor.schema, blockElements = schema.getBlockElements();
      var node = selection.getStart(), rootNode = editor.getBody(), rng;
      var startContainer, startOffset, endContainer, endOffset, rootBlockNode;
      var tempNode, wrapped, restoreSelection;
      var rootNodeName, forcedRootBlock;

      forcedRootBlock = settings.forced_root_block;

      if (!node || node.nodeType !== 1 || !forcedRootBlock) {
        return;
      }

      // Check if node is wrapped in block
      while (node && node !== rootNode) {
        if (blockElements[node.nodeName]) {
          return;
        }

        node = node.parentNode;
      }

      // Get current selection
      rng = selection.getRng();
      startContainer = rng.startContainer;
      startOffset = rng.startOffset;
      endContainer = rng.endContainer;
      endOffset = rng.endOffset;

      try {
        restoreSelection = editor.getDoc().activeElement === rootNode;
      } catch (ex) {
        // IE throws unspecified error here sometimes
      }

      // Wrap non block elements and text nodes
      node = rootNode.firstChild;
      rootNodeName = rootNode.nodeName.toLowerCase();
      while (node) {
        // TODO: Break this up, too complex
        if (((node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName]))) &&
          schema.isValidChild(rootNodeName, forcedRootBlock.toLowerCase())) {
          // Remove empty text nodes
          if (node.nodeType === 3 && node.nodeValue.length === 0) {
            tempNode = node;
            node = node.nextSibling;
            dom.remove(tempNode);
            continue;
          }

          if (!rootBlockNode) {
            rootBlockNode = dom.create(forcedRootBlock, editor.settings.forced_root_block_attrs);
            node.parentNode.insertBefore(rootBlockNode, node);
            wrapped = true;
          }

          tempNode = node;
          node = node.nextSibling;
          rootBlockNode.appendChild(tempNode);
        } else {
          rootBlockNode = null;
          node = node.nextSibling;
        }
      }

      if (wrapped && restoreSelection) {
        rng.setStart(startContainer, startOffset);
        rng.setEnd(endContainer, endOffset);
        selection.setRng(rng);
        editor.nodeChanged();
      }
    };

    var setup = function (editor) {
      if (editor.settings.forced_root_block) {
        editor.on('NodeChange', Fun.curry(addRootBlocks, editor));
      }
    };

    return {
      setup: setup
    };
  }
);
/**
 * Dimensions.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
 */

/**
 * This module measures nodes and returns client rects. The client rects has an
 * extra node property.
 *
 * @private
 * @class tinymce.dom.Dimensions
 */
define(
  'tinymce.core.dom.Dimensions',
  [
    "tinymce.core.util.Arr",
    "tinymce.core.dom.NodeType",
    "tinymce.core.geom.ClientRect"
  ],
  function (Arr, NodeType, ClientRect) {
    var getClientRects = function (node) {
      var toArrayWithNode = function (clientRects) {
        return Arr.map(clientRects, function (clientRect) {
          clientRect = ClientRect.clone(clientRect);
          clientRect.node = node;

          return clientRect;
        });
      };

      if (Arr.isArray(node)) {
        return Arr.reduce(node, function (result, node) {
          return result.concat(getClientRects(node));
        }, []);
      }

      if (NodeType.isElement(node)) {
        return toArrayWithNode(node.getClientRects());
      }

      if (NodeType.isText(node)) {
        var rng = node.ownerDocument.createRange();

        rng.setStart(node, 0);
        rng.setEnd(node, node.data.length);

        return toArrayWithNode(rng.getClientRects());
      }
    };

    return {
      /**
       * Returns the client rects for a specific node.
       *
       * @method getClientRects
       * @param {Array/DOMNode} node Node or array of nodes to get client rects on.
       * @param {Array} Array of client rects with a extra node property.
       */
      getClientRects: getClientRects
    };
  }
);
/**
 * LineUtils.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
 */

/**
 * Utility functions for working with lines.
 *
 * @private
 * @class tinymce.caret.LineUtils
 */
define(
  'tinymce.core.caret.LineUtils',
  [
    "tinymce.core.util.Fun",
    "tinymce.core.util.Arr",
    "tinymce.core.dom.NodeType",
    "tinymce.core.dom.Dimensions",
    "tinymce.core.geom.ClientRect",
    "tinymce.core.caret.CaretUtils",
    "tinymce.core.caret.CaretCandidate"
  ],
  function (Fun, Arr, NodeType, Dimensions, ClientRect, CaretUtils, CaretCandidate) {
    var isContentEditableFalse = NodeType.isContentEditableFalse,
      findNode = CaretUtils.findNode,
      curry = Fun.curry;

    var distanceToRectLeft = function (clientRect, clientX) {
      return Math.abs(clientRect.left - clientX);
    };

    var distanceToRectRight = function (clientRect, clientX) {
      return Math.abs(clientRect.right - clientX);
    };

    var findClosestClientRect = function (clientRects, clientX) {
      var isInside = function (clientX, clientRect) {
        return clientX >= clientRect.left && clientX <= clientRect.right;
      };

      return Arr.reduce(clientRects, function (oldClientRect, clientRect) {
        var oldDistance, newDistance;

        oldDistance = Math.min(distanceToRectLeft(oldClientRect, clientX), distanceToRectRight(oldClientRect, clientX));
        newDistance = Math.min(distanceToRectLeft(clientRect, clientX), distanceToRectRight(clientRect, clientX));

        if (isInside(clientX, clientRect)) {
          return clientRect;
        }

        if (isInside(clientX, oldClientRect)) {
          return oldClientRect;
        }

        // cE=false has higher priority
        if (newDistance == oldDistance && isContentEditableFalse(clientRect.node)) {
          return clientRect;
        }

        if (newDistance < oldDistance) {
          return clientRect;
        }

        return oldClientRect;
      });
    };

    var walkUntil = function (direction, rootNode, predicateFn, node) {
      while ((node = findNode(node, direction, CaretCandidate.isEditableCaretCandidate, rootNode))) {
        if (predicateFn(node)) {
          return;
        }
      }
    };

    var findLineNodeRects = function (rootNode, targetNodeRect) {
      var clientRects = [];

      var collect = function (checkPosFn, node) {
        var lineRects;

        lineRects = Arr.filter(Dimensions.getClientRects(node), function (clientRect) {
          return !checkPosFn(clientRect, targetNodeRect);
        });

        clientRects = clientRects.concat(lineRects);

        return lineRects.length === 0;
      };

      clientRects.push(targetNodeRect);
      walkUntil(-1, rootNode, curry(collect, ClientRect.isAbove), targetNodeRect.node);
      walkUntil(1, rootNode, curry(collect, ClientRect.isBelow), targetNodeRect.node);

      return clientRects;
    };

    var getContentEditableFalseChildren = function (rootNode) {
      return Arr.filter(Arr.toArray(rootNode.getElementsByTagName('*')), isContentEditableFalse);
    };

    var caretInfo = function (clientRect, clientX) {
      return {
        node: clientRect.node,
        before: distanceToRectLeft(clientRect, clientX) < distanceToRectRight(clientRect, clientX)
      };
    };

    var closestCaret = function (rootNode, clientX, clientY) {
      var contentEditableFalseNodeRects, closestNodeRect;

      contentEditableFalseNodeRects = Dimensions.getClientRects(getContentEditableFalseChildren(rootNode));
      contentEditableFalseNodeRects = Arr.filter(contentEditableFalseNodeRects, function (clientRect) {
        return clientY >= clientRect.top && clientY <= clientRect.bottom;
      });

      closestNodeRect = findClosestClientRect(contentEditableFalseNodeRects, clientX);
      if (closestNodeRect) {
        closestNodeRect = findClosestClientRect(findLineNodeRects(rootNode, closestNodeRect), clientX);
        if (closestNodeRect && isContentEditableFalse(closestNodeRect.node)) {
          return caretInfo(closestNodeRect, clientX);
        }
      }

      return null;
    };

    return {
      findClosestClientRect: findClosestClientRect,
      findLineNodeRects: findLineNodeRects,
      closestCaret: closestCaret
    };
  }
);
/**
 * LineWalker.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
 */

/**
 * This module lets you walk the document line by line
 * returing nodes and client rects for each line.
 *
 * @private
 * @class tinymce.caret.LineWalker
 */
define(
  'tinymce.core.caret.LineWalker',
  [
    "tinymce.core.util.Fun",
    "tinymce.core.util.Arr",
    "tinymce.core.dom.Dimensions",
    "tinymce.core.caret.CaretCandidate",
    "tinymce.core.caret.CaretUtils",
    "tinymce.core.caret.CaretWalker",
    "tinymce.core.caret.CaretPosition",
    "tinymce.core.geom.ClientRect"
  ],
  function (Fun, Arr, Dimensions, CaretCandidate, CaretUtils, CaretWalker, CaretPosition, ClientRect) {
    var curry = Fun.curry;

    var findUntil = function (direction, rootNode, predicateFn, node) {
      while ((node = CaretUtils.findNode(node, direction, CaretCandidate.isEditableCaretCandidate, rootNode))) {
        if (predicateFn(node)) {
          return;
        }
      }
    };

    var walkUntil = function (direction, isAboveFn, isBeflowFn, rootNode, predicateFn, caretPosition) {
      var line = 0, node, result = [], targetClientRect;

      var add = function (node) {
        var i, clientRect, clientRects;

        clientRects = Dimensions.getClientRects(node);
        if (direction == -1) {
          clientRects = clientRects.reverse();
        }

        for (i = 0; i < clientRects.length; i++) {
          clientRect = clientRects[i];
          if (isBeflowFn(clientRect, targetClientRect)) {
            continue;
          }

          if (result.length > 0 && isAboveFn(clientRect, Arr.last(result))) {
            line++;
          }

          clientRect.line = line;

          if (predicateFn(clientRect)) {
            return true;
          }

          result.push(clientRect);
        }
      };

      targetClientRect = Arr.last(caretPosition.getClientRects());
      if (!targetClientRect) {
        return result;
      }

      node = caretPosition.getNode();
      add(node);
      findUntil(direction, rootNode, add, node);

      return result;
    };

    var aboveLineNumber = function (lineNumber, clientRect) {
      return clientRect.line > lineNumber;
    };

    var isLine = function (lineNumber, clientRect) {
      return clientRect.line === lineNumber;
    };

    var upUntil = curry(walkUntil, -1, ClientRect.isAbove, ClientRect.isBelow);
    var downUntil = curry(walkUntil, 1, ClientRect.isBelow, ClientRect.isAbove);

    var positionsUntil = function (direction, rootNode, predicateFn, node) {
      var caretWalker = new CaretWalker(rootNode), walkFn, isBelowFn, isAboveFn,
        caretPosition, result = [], line = 0, clientRect, targetClientRect;

      var getClientRect = function (caretPosition) {
        if (direction == 1) {
          return Arr.last(caretPosition.getClientRects());
        }

        return Arr.last(caretPosition.getClientRects());
      };

      if (direction == 1) {
        walkFn = caretWalker.next;
        isBelowFn = ClientRect.isBelow;
        isAboveFn = ClientRect.isAbove;
        caretPosition = CaretPosition.after(node);
      } else {
        walkFn = caretWalker.prev;
        isBelowFn = ClientRect.isAbove;
        isAboveFn = ClientRect.isBelow;
        caretPosition = CaretPosition.before(node);
      }

      targetClientRect = getClientRect(caretPosition);

      do {
        if (!caretPosition.isVisible()) {
          continue;
        }

        clientRect = getClientRect(caretPosition);

        if (isAboveFn(clientRect, targetClientRect)) {
          continue;
        }

        if (result.length > 0 && isBelowFn(clientRect, Arr.last(result))) {
          line++;
        }

        clientRect = ClientRect.clone(clientRect);
        clientRect.position = caretPosition;
        clientRect.line = line;

        if (predicateFn(clientRect)) {
          return result;
        }

        result.push(clientRect);
      } while ((caretPosition = walkFn(caretPosition)));

      return result;
    };

    return {
      upUntil: upUntil,
      downUntil: downUntil,

      /**
       * Find client rects with line and caret position until the predicate returns true.
       *
       * @method positionsUntil
       * @param {Number} direction Direction forward/backward 1/-1.
       * @param {DOMNode} rootNode Root node to walk within.
       * @param {function} predicateFn Gets the client rect as it's input.
       * @param {DOMNode} node Node to start walking from.
       * @return {Array} Array of client rects with line and position properties.
       */
      positionsUntil: positionsUntil,

      isAboveLine: curry(aboveLineNumber),
      isLine: curry(isLine)
    };
  }
);
/**
 * CefUtils.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
 */

define(
  'tinymce.core.keyboard.CefUtils',
  [
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.caret.CaretUtils',
    'tinymce.core.dom.NodeType',
    'tinymce.core.util.Fun'
  ],
  function (CaretPosition, CaretUtils, NodeType, Fun) {
    var isContentEditableTrue = NodeType.isContentEditableTrue;
    var isContentEditableFalse = NodeType.isContentEditableFalse;

    var showCaret = function (direction, editor, node, before) {
      // TODO: Figure out a better way to handle this dependency
      return editor._selectionOverrides.showCaret(direction, node, before);
    };

    var getNodeRange = function (node) {
      var rng = node.ownerDocument.createRange();
      rng.selectNode(node);
      return rng;
    };

    var selectNode = function (editor, node) {
      var e;

      e = editor.fire('BeforeObjectSelected', { target: node });
      if (e.isDefaultPrevented()) {
        return null;
      }

      return getNodeRange(node);
    };

    var renderCaretAtRange = function (editor, range) {
      var caretPosition, ceRoot;

      range = CaretUtils.normalizeRange(1, editor.getBody(), range);
      caretPosition = CaretPosition.fromRangeStart(range);

      if (isContentEditableFalse(caretPosition.getNode())) {
        return showCaret(1, editor, caretPosition.getNode(), !caretPosition.isAtEnd());
      }

      if (isContentEditableFalse(caretPosition.getNode(true))) {
        return showCaret(1, editor, caretPosition.getNode(true), false);
      }

      // TODO: Should render caret before/after depending on where you click on the page forces after now
      ceRoot = editor.dom.getParent(caretPosition.getNode(), Fun.or(isContentEditableFalse, isContentEditableTrue));
      if (isContentEditableFalse(ceRoot)) {
        return showCaret(1, editor, ceRoot, false);
      }

      return null;
    };

    var renderRangeCaret = function (editor, range) {
      var caretRange;

      if (!range || !range.collapsed) {
        return range;
      }

      caretRange = renderCaretAtRange(editor, range);
      if (caretRange) {
        return caretRange;
      }

      return range;
    };

    return {
      showCaret: showCaret,
      selectNode: selectNode,
      renderCaretAtRange: renderCaretAtRange,
      renderRangeCaret: renderRangeCaret
    };
  }
);

/**
 * CefNavigation.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
 */

define(
  'tinymce.core.keyboard.CefNavigation',
  [
    'tinymce.core.caret.CaretContainer',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.caret.CaretUtils',
    'tinymce.core.caret.CaretWalker',
    'tinymce.core.caret.LineUtils',
    'tinymce.core.caret.LineWalker',
    'tinymce.core.dom.NodeType',
    'tinymce.core.dom.RangeUtils',
    'tinymce.core.Env',
    'tinymce.core.keyboard.CefUtils',
    'tinymce.core.util.Arr',
    'tinymce.core.util.Fun'
  ],
  function (CaretContainer, CaretPosition, CaretUtils, CaretWalker, LineUtils, LineWalker, NodeType, RangeUtils, Env, CefUtils, Arr, Fun) {
    var isContentEditableFalse = NodeType.isContentEditableFalse;
    var getSelectedNode = RangeUtils.getSelectedNode;
    var isAfterContentEditableFalse = CaretUtils.isAfterContentEditableFalse;
    var isBeforeContentEditableFalse = CaretUtils.isBeforeContentEditableFalse;

    var getVisualCaretPosition = function (walkFn, caretPosition) {
      while ((caretPosition = walkFn(caretPosition))) {
        if (caretPosition.isVisible()) {
          return caretPosition;
        }
      }

      return caretPosition;
    };

    var isMoveInsideSameBlock = function (fromCaretPosition, toCaretPosition) {
      var inSameBlock = CaretUtils.isInSameBlock(fromCaretPosition, toCaretPosition);

      // Handle bogus BR <p>abc|<br></p>
      if (!inSameBlock && NodeType.isBr(fromCaretPosition.getNode())) {
        return true;
      }

      return inSameBlock;
    };

    var isRangeInCaretContainerBlock = function (range) {
      return CaretContainer.isCaretContainerBlock(range.startContainer);
    };

    var getNormalizedRangeEndPoint = function (direction, rootNode, range) {
      range = CaretUtils.normalizeRange(direction, rootNode, range);

      if (direction === -1) {
        return CaretPosition.fromRangeStart(range);
      }

      return CaretPosition.fromRangeEnd(range);
    };

    var moveToCeFalseHorizontally = function (direction, editor, getNextPosFn, isBeforeContentEditableFalseFn, range) {
      var node, caretPosition, peekCaretPosition, rangeIsInContainerBlock;

      if (!range.collapsed) {
        node = getSelectedNode(range);
        if (isContentEditableFalse(node)) {
          return CefUtils.showCaret(direction, editor, node, direction === -1);
        }
      }

      rangeIsInContainerBlock = isRangeInCaretContainerBlock(range);
      caretPosition = getNormalizedRangeEndPoint(direction, editor.getBody(), range);

      if (isBeforeContentEditableFalseFn(caretPosition)) {
        return CefUtils.selectNode(editor, caretPosition.getNode(direction === -1));
      }

      caretPosition = getNextPosFn(caretPosition);
      if (!caretPosition) {
        if (rangeIsInContainerBlock) {
          return range;
        }

        return null;
      }

      if (isBeforeContentEditableFalseFn(caretPosition)) {
        return CefUtils.showCaret(direction, editor, caretPosition.getNode(direction === -1), direction === 1);
      }

      // Peek ahead for handling of ab|c<span cE=false> -> abc|<span cE=false>
      peekCaretPosition = getNextPosFn(caretPosition);
      if (isBeforeContentEditableFalseFn(peekCaretPosition)) {
        if (isMoveInsideSameBlock(caretPosition, peekCaretPosition)) {
          return CefUtils.showCaret(direction, editor, peekCaretPosition.getNode(direction === -1), direction === 1);
        }
      }

      if (rangeIsInContainerBlock) {
        return CefUtils.renderRangeCaret(editor, caretPosition.toRange());
      }

      return null;
    };

    var moveToCeFalseVertically = function (direction, editor, walkerFn, range) {
      var caretPosition, linePositions, nextLinePositions,
        closestNextLineRect, caretClientRect, clientX,
        dist1, dist2, contentEditableFalseNode;

      contentEditableFalseNode = getSelectedNode(range);
      caretPosition = getNormalizedRangeEndPoint(direction, editor.getBody(), range);
      linePositions = walkerFn(editor.getBody(), LineWalker.isAboveLine(1), caretPosition);
      nextLinePositions = Arr.filter(linePositions, LineWalker.isLine(1));
      caretClientRect = Arr.last(caretPosition.getClientRects());

      if (isBeforeContentEditableFalse(caretPosition)) {
        contentEditableFalseNode = caretPosition.getNode();
      }

      if (isAfterContentEditableFalse(caretPosition)) {
        contentEditableFalseNode = caretPosition.getNode(true);
      }

      if (!caretClientRect) {
        return null;
      }

      clientX = caretClientRect.left;

      closestNextLineRect = LineUtils.findClosestClientRect(nextLinePositions, clientX);
      if (closestNextLineRect) {
        if (isContentEditableFalse(closestNextLineRect.node)) {
          dist1 = Math.abs(clientX - closestNextLineRect.left);
          dist2 = Math.abs(clientX - closestNextLineRect.right);

          return CefUtils.showCaret(direction, editor, closestNextLineRect.node, dist1 < dist2);
        }
      }

      if (contentEditableFalseNode) {
        var caretPositions = LineWalker.positionsUntil(direction, editor.getBody(), LineWalker.isAboveLine(1), contentEditableFalseNode);

        closestNextLineRect = LineUtils.findClosestClientRect(Arr.filter(caretPositions, LineWalker.isLine(1)), clientX);
        if (closestNextLineRect) {
          return CefUtils.renderRangeCaret(editor, closestNextLineRect.position.toRange());
        }

        closestNextLineRect = Arr.last(Arr.filter(caretPositions, LineWalker.isLine(0)));
        if (closestNextLineRect) {
          return CefUtils.renderRangeCaret(editor, closestNextLineRect.position.toRange());
        }
      }
    };

    var createTextBlock = function (editor) {
      var textBlock = editor.dom.create(editor.settings.forced_root_block);

      if (!Env.ie || Env.ie >= 11) {
        textBlock.innerHTML = '<br data-mce-bogus="1">';
      }

      return textBlock;
    };

    var exitPreBlock = function (editor, direction, range) {
      var pre, caretPos, newBlock;
      var caretWalker = new CaretWalker(editor.getBody());
      var getNextVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.next);
      var getPrevVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.prev);

      if (range.collapsed && editor.settings.forced_root_block) {
        pre = editor.dom.getParent(range.startContainer, 'PRE');
        if (!pre) {
          return;
        }

        if (direction === 1) {
          caretPos = getNextVisualCaretPosition(CaretPosition.fromRangeStart(range));
        } else {
          caretPos = getPrevVisualCaretPosition(CaretPosition.fromRangeStart(range));
        }

        if (!caretPos) {
          newBlock = createTextBlock(editor);

          if (direction === 1) {
            editor.$(pre).after(newBlock);
          } else {
            editor.$(pre).before(newBlock);
          }

          editor.selection.select(newBlock, true);
          editor.selection.collapse();
        }
      }
    };

    var getHorizontalRange = function (editor, forward) {
      var caretWalker = new CaretWalker(editor.getBody());
      var getNextVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.next);
      var getPrevVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.prev);
      var newRange, direction = forward ? 1 : -1;
      var getNextPosFn = forward ? getNextVisualCaretPosition : getPrevVisualCaretPosition;
      var isBeforeContentEditableFalseFn = forward ? isBeforeContentEditableFalse : isAfterContentEditableFalse;
      var range = editor.selection.getRng();

      newRange = moveToCeFalseHorizontally(direction, editor, getNextPosFn, isBeforeContentEditableFalseFn, range);
      if (newRange) {
        return newRange;
      }

      newRange = exitPreBlock(editor, direction, range);
      if (newRange) {
        return newRange;
      }

      return null;
    };

    var getVerticalRange = function (editor, down) {
      var newRange, direction = down ? 1 : -1;
      var walkerFn = down ? LineWalker.downUntil : LineWalker.upUntil;
      var range = editor.selection.getRng();

      newRange = moveToCeFalseVertically(direction, editor, walkerFn, range);
      if (newRange) {
        return newRange;
      }

      newRange = exitPreBlock(editor, direction, range);
      if (newRange) {
        return newRange;
      }

      return null;
    };

    var moveH = function (editor, forward) {
      return function () {
        var newRng = getHorizontalRange(editor, forward);

        if (newRng) {
          editor.selection.setRng(newRng);
          return true;
        } else {
          return false;
        }
      };
    };

    var moveV = function (editor, down) {
      return function () {
        var newRng = getVerticalRange(editor, down);

        if (newRng) {
          editor.selection.setRng(newRng);
          return true;
        } else {
          return false;
        }
      };
    };

    return {
      moveH: moveH,
      moveV: moveV
    };
  }
);

define(
  'ephox.katamari.api.Merger',

  [
    'ephox.katamari.api.Type',
    'global!Array',
    'global!Error'
  ],

  function (Type, Array, Error) {

    var shallow = function (old, nu) {
      return nu;
    };

    var deep = function (old, nu) {
      var bothObjects = Type.isObject(old) && Type.isObject(nu);
      return bothObjects ? deepMerge(old, nu) : nu;
    };

    var baseMerge = function (merger) {
      return function() {
        // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome
        var objects = new Array(arguments.length);
        for (var i = 0; i < objects.length; i++) objects[i] = arguments[i];

        if (objects.length === 0) throw new Error('Can\'t merge zero objects');

        var ret = {};
        for (var j = 0; j < objects.length; j++) {
          var curObject = objects[j];
          for (var key in curObject) if (curObject.hasOwnProperty(key)) {
            ret[key] = merger(ret[key], curObject[key]);
          }
        }
        return ret;
      };
    };

    var deepMerge = baseMerge(deep);
    var merge = baseMerge(shallow);

    return {
      deepMerge: deepMerge,
      merge: merge
    };
  }
);
/**
 * MatchKeys.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
 */

define(
  'tinymce.core.keyboard.MatchKeys',
  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Merger'
  ],
  function (Arr, Fun, Merger) {
    var defaultPatterns = function (patterns) {
      return Arr.map(patterns, function (pattern) {
        return Merger.merge({
          shiftKey: false,
          altKey: false,
          ctrlKey: false,
          metaKey: false,
          keyCode: 0,
          action: Fun.noop
        }, pattern);
      });
    };

    var matchesEvent = function (pattern, evt) {
      return (
        evt.keyCode === pattern.keyCode &&
        evt.shiftKey === pattern.shiftKey &&
        evt.altKey === pattern.altKey &&
        evt.ctrlKey === pattern.ctrlKey &&
        evt.metaKey === pattern.metaKey
      );
    };

    var match = function (patterns, evt) {
      return Arr.bind(defaultPatterns(patterns), function (pattern) {
        return matchesEvent(pattern, evt) ? [pattern] : [ ];
      });
    };

    var action = function (f) {
      var args = Array.prototype.slice.call(arguments, 1);
      return function () {
        return f.apply(null, args);
      };
    };

    var execute = function (patterns, evt) {
      return Arr.find(match(patterns, evt), function (pattern) {
        return pattern.action();
      });
    };

    return {
      match: match,
      action: action,
      execute: execute
    };
  }
);
/**
 * ArrowKeys.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
 */

define(
  'tinymce.core.keyboard.ArrowKeys',
  [
    'tinymce.core.keyboard.BoundarySelection',
    'tinymce.core.keyboard.CefNavigation',
    'tinymce.core.keyboard.MatchKeys',
    'tinymce.core.util.VK'
  ],
  function (BoundarySelection, CefNavigation, MatchKeys, VK) {
    var executeKeydownOverride = function (editor, caret, evt) {
      MatchKeys.execute([
        { keyCode: VK.RIGHT, action: CefNavigation.moveH(editor, true) },
        { keyCode: VK.LEFT, action: CefNavigation.moveH(editor, false) },
        { keyCode: VK.UP, action: CefNavigation.moveV(editor, false) },
        { keyCode: VK.DOWN, action: CefNavigation.moveV(editor, true) },
        { keyCode: VK.RIGHT, action: BoundarySelection.move(editor, caret, true) },
        { keyCode: VK.LEFT, action: BoundarySelection.move(editor, caret, false) }
      ], evt).each(function (_) {
        evt.preventDefault();
      });
    };

    var setup = function (editor, caret) {
      editor.on('keydown', function (evt) {
        if (evt.isDefaultPrevented() === false) {
          executeKeydownOverride(editor, caret, evt);
        }
      });
    };

    return {
      setup: setup
    };
  }
);

/**
 * DeleteBackspaceKeys.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
 */

define(
  'tinymce.core.keyboard.DeleteBackspaceKeys',
  [
    'tinymce.core.delete.BlockBoundaryDelete',
    'tinymce.core.delete.BlockRangeDelete',
    'tinymce.core.delete.CefDelete',
    'tinymce.core.delete.InlineBoundaryDelete',
    'tinymce.core.delete.TableDelete',
    'tinymce.core.keyboard.MatchKeys',
    'tinymce.core.util.VK'
  ],
  function (BlockBoundaryDelete, BlockRangeDelete, CefDelete, InlineBoundaryDelete, TableDelete, MatchKeys, VK) {
    var executeKeydownOverride = function (editor, caret, evt) {
      MatchKeys.execute([
        { keyCode: VK.BACKSPACE, action: MatchKeys.action(CefDelete.backspaceDelete, editor, false) },
        { keyCode: VK.DELETE, action: MatchKeys.action(CefDelete.backspaceDelete, editor, true) },
        { keyCode: VK.BACKSPACE, action: MatchKeys.action(InlineBoundaryDelete.backspaceDelete, editor, caret, false) },
        { keyCode: VK.DELETE, action: MatchKeys.action(InlineBoundaryDelete.backspaceDelete, editor, caret, true) },
        { keyCode: VK.BACKSPACE, action: MatchKeys.action(BlockRangeDelete.backspaceDelete, editor, false) },
        { keyCode: VK.DELETE, action: MatchKeys.action(BlockRangeDelete.backspaceDelete, editor, true) },
        { keyCode: VK.BACKSPACE, action: MatchKeys.action(BlockBoundaryDelete.backspaceDelete, editor, false) },
        { keyCode: VK.DELETE, action: MatchKeys.action(BlockBoundaryDelete.backspaceDelete, editor, true) },
        { keyCode: VK.BACKSPACE, action: MatchKeys.action(TableDelete.backspaceDelete, editor, false) },
        { keyCode: VK.DELETE, action: MatchKeys.action(TableDelete.backspaceDelete, editor, true) }
      ], evt).each(function (_) {
        evt.preventDefault();
      });
    };

    var executeKeyupOverride = function (editor, evt) {
      MatchKeys.execute([
        { keyCode: VK.BACKSPACE, action: MatchKeys.action(CefDelete.paddEmptyElement, editor) },
        { keyCode: VK.DELETE, action: MatchKeys.action(CefDelete.paddEmptyElement, editor) }
      ], evt);
    };

    var setup = function (editor, caret) {
      editor.on('keydown', function (evt) {
        if (evt.isDefaultPrevented() === false) {
          executeKeydownOverride(editor, caret, evt);
        }
      });

      editor.on('keyup', function (evt) {
        if (evt.isDefaultPrevented() === false) {
          executeKeyupOverride(editor, evt);
        }
      });
    };

    return {
      setup: setup
    };
  }
);

/**
 * InsertNewLine.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
 */

define(
  'tinymce.core.keyboard.InsertNewLine',
  [
    'tinymce.core.caret.CaretContainer',
    'tinymce.core.dom.NodeType',
    'tinymce.core.dom.RangeUtils',
    'tinymce.core.dom.TreeWalker',
    'tinymce.core.text.Zwsp',
    'tinymce.core.util.Tools'
  ],
  function (CaretContainer, NodeType, RangeUtils, TreeWalker, Zwsp, Tools) {
    var isEmptyAnchor = function (elm) {
      return elm && elm.nodeName === "A" && Tools.trim(Zwsp.trim(elm.innerText || elm.textContent)).length === 0;
    };

    var isTableCell = function (node) {
      return node && /^(TD|TH|CAPTION)$/.test(node.nodeName);
    };

    var hasFirstChild = function (elm, name) {
      return elm.firstChild && elm.firstChild.nodeName == name;
    };

    var hasParent = function (elm, parentName) {
      return elm && elm.parentNode && elm.parentNode.nodeName === parentName;
    };

    var emptyBlock = function (elm) {
      elm.innerHTML = '<br data-mce-bogus="1">';
    };

    var containerAndSiblingName = function (container, nodeName) {
      return container.nodeName === nodeName || (container.previousSibling && container.previousSibling.nodeName === nodeName);
    };

    var isListBlock = function (elm) {
      return elm && /^(OL|UL|LI)$/.test(elm.nodeName);
    };

    var isNestedList = function (elm) {
      return isListBlock(elm) && isListBlock(elm.parentNode);
    };

    // Returns true if the block can be split into two blocks or not
    var canSplitBlock = function (dom, node) {
      return node &&
        dom.isBlock(node) &&
        !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) &&
        !/^(fixed|absolute)/i.test(node.style.position) &&
        dom.getContentEditable(node) !== "true";
    };

    // Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p>
    var trimInlineElementsOnLeftSideOfBlock = function (dom, nonEmptyElementsMap, block) {
      var node = block, firstChilds = [], i;

      if (!node) {
        return;
      }

      // Find inner most first child ex: <p><i><b>*</b></i></p>
      while ((node = node.firstChild)) {
        if (dom.isBlock(node)) {
          return;
        }

        if (node.nodeType == 1 && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
          firstChilds.push(node);
        }
      }

      i = firstChilds.length;
      while (i--) {
        node = firstChilds[i];
        if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) {
          dom.remove(node);
        } else {
          if (isEmptyAnchor(node)) {
            dom.remove(node);
          }
        }
      }
    };

    var normalizeZwspOffset = function (start, container, offset) {
      if (NodeType.isText(container) === false) {
        return offset;
      } if (start) {
        return offset === 1 && container.data.charAt(offset - 1) === Zwsp.ZWSP ? 0 : offset;
      } else {
        return offset === container.data.length - 1 && container.data.charAt(offset) === Zwsp.ZWSP ? container.data.length : offset;
      }
    };

    var includeZwspInRange = function (rng) {
      var newRng = rng.cloneRange();
      newRng.setStart(rng.startContainer, normalizeZwspOffset(true, rng.startContainer, rng.startOffset));
      newRng.setEnd(rng.endContainer, normalizeZwspOffset(false, rng.endContainer, rng.endOffset));
      return newRng;
    };

    var firstNonWhiteSpaceNodeSibling = function (node) {
      while (node) {
        if (node.nodeType === 1 || (node.nodeType === 3 && node.data && /[\r\n\s]/.test(node.data))) {
          return node;
        }

        node = node.nextSibling;
      }
    };

    // Inserts a BR element if the forced_root_block option is set to false or empty string
    var insertBr = function (editor, evt) {
      editor.execCommand("InsertLineBreak", false, evt);
    };

    // Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element
    var trimLeadingLineBreaks = function (node) {
      do {
        if (node.nodeType === 3) {
          node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, '');
        }

        node = node.firstChild;
      } while (node);
    };

    var getEditableRoot = function (dom, node) {
      var root = dom.getRoot(), parent, editableRoot;

      // Get all parents until we hit a non editable parent or the root
      parent = node;
      while (parent !== root && dom.getContentEditable(parent) !== "false") {
        if (dom.getContentEditable(parent) === "true") {
          editableRoot = parent;
        }

        parent = parent.parentNode;
      }

      return parent !== root ? editableRoot : root;
    };

    var setForcedBlockAttrs = function (editor, node) {
      var forcedRootBlockName = editor.settings.forced_root_block;

      if (forcedRootBlockName && forcedRootBlockName.toLowerCase() === node.tagName.toLowerCase()) {
        editor.dom.setAttribs(node, editor.settings.forced_root_block_attrs);
      }
    };

    // Wraps any text nodes or inline elements in the specified forced root block name
    var wrapSelfAndSiblingsInDefaultBlock = function (editor, newBlockName, rng, container, offset) {
      var newBlock, parentBlock, startNode, node, next, rootBlockName, blockName = newBlockName || 'P';
      var dom = editor.dom, editableRoot = getEditableRoot(dom, container);

      // Not in a block element or in a table cell or caption
      parentBlock = dom.getParent(container, dom.isBlock);
      if (!parentBlock || !canSplitBlock(dom, parentBlock)) {
        parentBlock = parentBlock || editableRoot;

        if (parentBlock == editor.getBody() || isTableCell(parentBlock)) {
          rootBlockName = parentBlock.nodeName.toLowerCase();
        } else {
          rootBlockName = parentBlock.parentNode.nodeName.toLowerCase();
        }

        if (!parentBlock.hasChildNodes()) {
          newBlock = dom.create(blockName);
          setForcedBlockAttrs(editor, newBlock);
          parentBlock.appendChild(newBlock);
          rng.setStart(newBlock, 0);
          rng.setEnd(newBlock, 0);
          return newBlock;
        }

        // Find parent that is the first child of parentBlock
        node = container;
        while (node.parentNode != parentBlock) {
          node = node.parentNode;
        }

        // Loop left to find start node start wrapping at
        while (node && !dom.isBlock(node)) {
          startNode = node;
          node = node.previousSibling;
        }

        if (startNode && editor.schema.isValidChild(rootBlockName, blockName.toLowerCase())) {
          newBlock = dom.create(blockName);
          setForcedBlockAttrs(editor, newBlock);
          startNode.parentNode.insertBefore(newBlock, startNode);

          // Start wrapping until we hit a block
          node = startNode;
          while (node && !dom.isBlock(node)) {
            next = node.nextSibling;
            newBlock.appendChild(node);
            node = next;
          }

          // Restore range to it's past location
          rng.setStart(container, offset);
          rng.setEnd(container, offset);
        }
      }

      return container;
    };

    // Adds a BR at the end of blocks that only contains an IMG or INPUT since
    // these might be floated and then they won't expand the block
    var addBrToBlockIfNeeded = function (dom, block) {
      var lastChild;

      // IE will render the blocks correctly other browsers needs a BR
      block.normalize(); // Remove empty text nodes that got left behind by the extract

      // Check if the block is empty or contains a floated last child
      lastChild = block.lastChild;
      if (!lastChild || (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) {
        dom.add(block, 'br');
      }
    };

    var getContainerBlock = function (containerBlock) {
      var containerBlockParent = containerBlock.parentNode;

      if (/^(LI|DT|DD)$/.test(containerBlockParent.nodeName)) {
        return containerBlockParent;
      }

      return containerBlock;
    };

    var isFirstOrLastLi = function (containerBlock, parentBlock, first) {
      var node = containerBlock[first ? 'firstChild' : 'lastChild'];

      // Find first/last element since there might be whitespace there
      while (node) {
        if (node.nodeType == 1) {
          break;
        }

        node = node[first ? 'nextSibling' : 'previousSibling'];
      }

      return node === parentBlock;
    };

    var insert = function (editor, evt) {
      var tmpRng, editableRoot, container, offset, parentBlock, shiftKey;
      var newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer;
      var dom = editor.dom, selection = editor.selection, settings = editor.settings;
      var schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements();
      var rng = editor.selection.getRng();

      // Moves the caret to a suitable position within the root for example in the first non
      // pure whitespace text node or before an image
      var moveToCaretPosition = function (root) {
        var walker, node, rng, lastNode = root, tempElm;
        var moveCaretBeforeOnEnterElementsMap = schema.getMoveCaretBeforeOnEnterElements();

        if (!root) {
          return;
        }

        if (/^(LI|DT|DD)$/.test(root.nodeName)) {
          var firstChild = firstNonWhiteSpaceNodeSibling(root.firstChild);

          if (firstChild && /^(UL|OL|DL)$/.test(firstChild.nodeName)) {
            root.insertBefore(dom.doc.createTextNode('\u00a0'), root.firstChild);
          }
        }

        rng = dom.createRng();
        root.normalize();

        if (root.hasChildNodes()) {
          walker = new TreeWalker(root, root);

          while ((node = walker.current())) {
            if (node.nodeType == 3) {
              rng.setStart(node, 0);
              rng.setEnd(node, 0);
              break;
            }

            if (moveCaretBeforeOnEnterElementsMap[node.nodeName.toLowerCase()]) {
              rng.setStartBefore(node);
              rng.setEndBefore(node);
              break;
            }

            lastNode = node;
            node = walker.next();
          }

          if (!node) {
            rng.setStart(lastNode, 0);
            rng.setEnd(lastNode, 0);
          }
        } else {
          if (root.nodeName == 'BR') {
            if (root.nextSibling && dom.isBlock(root.nextSibling)) {
              rng.setStartBefore(root);
              rng.setEndBefore(root);
            } else {
              rng.setStartAfter(root);
              rng.setEndAfter(root);
            }
          } else {
            rng.setStart(root, 0);
            rng.setEnd(root, 0);
          }
        }

        selection.setRng(rng);

        // Remove tempElm created for old IE:s
        dom.remove(tempElm);
        selection.scrollIntoView(root);
      };

      // Creates a new block element by cloning the current one or creating a new one if the name is specified
      // This function will also copy any text formatting from the parent block and add it to the new one
      var createNewBlock = function (name) {
        var node = container, block, clonedNode, caretNode, textInlineElements = schema.getTextInlineElements();

        if (name || parentBlockName == "TABLE" || parentBlockName == "HR") {
          block = dom.create(name || newBlockName);
          setForcedBlockAttrs(editor, block);
        } else {
          block = parentBlock.cloneNode(false);
        }

        caretNode = block;

        if (settings.keep_styles === false) {
          dom.setAttrib(block, 'style', null); // wipe out any styles that came over with the block
          dom.setAttrib(block, 'class', null);
        } else {
          // Clone any parent styles
          do {
            if (textInlineElements[node.nodeName]) {
              // Never clone a caret containers
              if (node.id == '_mce_caret') {
                continue;
              }

              clonedNode = node.cloneNode(false);
              dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique

              if (block.hasChildNodes()) {
                clonedNode.appendChild(block.firstChild);
                block.appendChild(clonedNode);
              } else {
                caretNode = clonedNode;
                block.appendChild(clonedNode);
              }
            }
          } while ((node = node.parentNode) && node != editableRoot);
        }

        emptyBlock(caretNode);

        return block;
      };

      // Returns true/false if the caret is at the start/end of the parent block element
      var isCaretAtStartOrEndOfBlock = function (start) {
        var walker, node, name, normalizedOffset;

        normalizedOffset = normalizeZwspOffset(start, container, offset);

        // Caret is in the middle of a text node like "a|b"
        if (container.nodeType == 3 && (start ? normalizedOffset > 0 : normalizedOffset < container.nodeValue.length)) {
          return false;
        }

        // If after the last element in block node edge case for #5091
        if (container.parentNode == parentBlock && isAfterLastNodeInContainer && !start) {
          return true;
        }

        // If the caret if before the first element in parentBlock
        if (start && container.nodeType == 1 && container == parentBlock.firstChild) {
          return true;
        }

        // Caret can be before/after a table or a hr
        if (containerAndSiblingName(container, 'TABLE') || containerAndSiblingName(container, 'HR')) {
          return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start);
        }

        // Walk the DOM and look for text nodes or non empty elements
        walker = new TreeWalker(container, parentBlock);

        // If caret is in beginning or end of a text block then jump to the next/previous node
        if (container.nodeType == 3) {
          if (start && normalizedOffset === 0) {
            walker.prev();
          } else if (!start && normalizedOffset == container.nodeValue.length) {
            walker.next();
          }
        }

        while ((node = walker.current())) {
          if (node.nodeType === 1) {
            // Ignore bogus elements
            if (!node.getAttribute('data-mce-bogus')) {
              // Keep empty elements like <img /> <input /> but not trailing br:s like <p>text|<br></p>
              name = node.nodeName.toLowerCase();
              if (nonEmptyElementsMap[name] && name !== 'br') {
                return false;
              }
            }
          } else if (node.nodeType === 3 && !/^[ \t\r\n]*$/.test(node.nodeValue)) {
            return false;
          }

          if (start) {
            walker.prev();
          } else {
            walker.next();
          }
        }

        return true;
      };

      // Inserts a block or br before/after or in the middle of a split list of the LI is empty
      var handleEmptyListItem = function () {
        if (containerBlock == editor.getBody()) {
          return;
        }

        if (isNestedList(containerBlock)) {
          newBlockName = 'LI';
        }

        newBlock = newBlockName ? createNewBlock(newBlockName) : dom.create('BR');

        if (isFirstOrLastLi(containerBlock, parentBlock, true) && isFirstOrLastLi(containerBlock, parentBlock, false)) {
          if (hasParent(containerBlock, 'LI')) {
            // Nested list is inside a LI
            dom.insertAfter(newBlock, getContainerBlock(containerBlock));
          } else {
            // Is first and last list item then replace the OL/UL with a text block
            dom.replace(newBlock, containerBlock);
          }
        } else if (isFirstOrLastLi(containerBlock, parentBlock, true)) {
          if (hasParent(containerBlock, 'LI')) {
            // List nested in an LI then move the list to a new sibling LI
            dom.insertAfter(newBlock, getContainerBlock(containerBlock));
            newBlock.appendChild(dom.doc.createTextNode(' ')); // Needed for IE so the caret can be placed
            newBlock.appendChild(containerBlock);
          } else {
            // First LI in list then remove LI and add text block before list
            containerBlock.parentNode.insertBefore(newBlock, containerBlock);
          }
        } else if (isFirstOrLastLi(containerBlock, parentBlock, false)) {
          // Last LI in list then remove LI and add text block after list
          dom.insertAfter(newBlock, getContainerBlock(containerBlock));
        } else {
          // Middle LI in list the split the list and insert a text block in the middle
          // Extract after fragment and insert it after the current block
          containerBlock = getContainerBlock(containerBlock);
          tmpRng = rng.cloneRange();
          tmpRng.setStartAfter(parentBlock);
          tmpRng.setEndAfter(containerBlock);
          fragment = tmpRng.extractContents();

          if (newBlockName === 'LI' && hasFirstChild(fragment, 'LI')) {
            newBlock = fragment.firstChild;
            dom.insertAfter(fragment, containerBlock);
          } else {
            dom.insertAfter(fragment, containerBlock);
            dom.insertAfter(newBlock, containerBlock);
          }
        }

        dom.remove(parentBlock);
        moveToCaretPosition(newBlock);
      };

      var insertNewBlockAfter = function () {
        // If the caret is at the end of a header we produce a P tag after it similar to Word unless we are in a hgroup
        if (/^(H[1-6]|PRE|FIGURE)$/.test(parentBlockName) && containerBlockName != 'HGROUP') {
          newBlock = createNewBlock(newBlockName);
        } else {
          newBlock = createNewBlock();
        }

        // Split the current container block element if enter is pressed inside an empty inner block element
        if (settings.end_container_on_empty_block && canSplitBlock(dom, containerBlock) && dom.isEmpty(parentBlock)) {
          // Split container block for example a BLOCKQUOTE at the current blockParent location for example a P
          newBlock = dom.split(containerBlock, parentBlock);
        } else {
          dom.insertAfter(newBlock, parentBlock);
        }

        moveToCaretPosition(newBlock);
      };

      // Setup range items and newBlockName
      new RangeUtils(dom).normalize(rng);
      container = rng.startContainer;
      offset = rng.startOffset;
      newBlockName = (settings.force_p_newlines ? 'p' : '') || settings.forced_root_block;
      newBlockName = newBlockName ? newBlockName.toUpperCase() : '';
      shiftKey = evt.shiftKey;

      // Resolve node index
      if (container.nodeType == 1 && container.hasChildNodes()) {
        isAfterLastNodeInContainer = offset > container.childNodes.length - 1;

        container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container;
        if (isAfterLastNodeInContainer && container.nodeType == 3) {
          offset = container.nodeValue.length;
        } else {
          offset = 0;
        }
      }

      // Get editable root node, normally the body element but sometimes a div or span
      editableRoot = getEditableRoot(dom, container);

      // If there is no editable root then enter is done inside a contentEditable false element
      if (!editableRoot) {
        return;
      }

      // If editable root isn't block nor the root of the editor
      if (!dom.isBlock(editableRoot) && editableRoot != dom.getRoot()) {
        if (!newBlockName || shiftKey) {
          insertBr(editor, evt);
        }

        return;
      }

      // Wrap the current node and it's sibling in a default block if it's needed.
      // for example this <td>text|<b>text2</b></td> will become this <td><p>text|<b>text2</p></b></td>
      // This won't happen if root blocks are disabled or the shiftKey is pressed
      if ((newBlockName && !shiftKey) || (!newBlockName && shiftKey)) {
        container = wrapSelfAndSiblingsInDefaultBlock(editor, newBlockName, rng, container, offset);
      }

      // Find parent block and setup empty block paddings
      parentBlock = dom.getParent(container, dom.isBlock);
      containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null;

      // Setup block names
      parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
      containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5

      // Enter inside block contained within a LI then split or insert before/after LI
      if (containerBlockName == 'LI' && !evt.ctrlKey) {
        parentBlock = containerBlock;
        containerBlock = containerBlock.parentNode;
        parentBlockName = containerBlockName;
      }

      // Handle enter in list item
      if (/^(LI|DT|DD)$/.test(parentBlockName)) {
        if (!newBlockName && shiftKey) {
          insertBr(editor, evt);
          return;
        }

        // Handle enter inside an empty list item
        if (dom.isEmpty(parentBlock)) {
          handleEmptyListItem();
          return;
        }
      }

      // Don't split PRE tags but insert a BR instead easier when writing code samples etc
      if (parentBlockName == 'PRE' && settings.br_in_pre !== false) {
        if (!shiftKey) {
          insertBr(editor, evt);
          return;
        }
      } else {
        // If no root block is configured then insert a BR by default or if the shiftKey is pressed
        if ((!newBlockName && !shiftKey && parentBlockName != 'LI') || (newBlockName && shiftKey)) {
          insertBr(editor, evt);
          return;
        }
      }

      // If parent block is root then never insert new blocks
      if (newBlockName && parentBlock === editor.getBody()) {
        return;
      }

      // Default block name if it's not configured
      newBlockName = newBlockName || 'P';

      // Insert new block before/after the parent block depending on caret location
      if (CaretContainer.isCaretContainerBlock(parentBlock)) {
        newBlock = CaretContainer.showCaretContainerBlock(parentBlock);
        if (dom.isEmpty(parentBlock)) {
          emptyBlock(parentBlock);
        }
        moveToCaretPosition(newBlock);
      } else if (isCaretAtStartOrEndOfBlock()) {
        insertNewBlockAfter();
      } else if (isCaretAtStartOrEndOfBlock(true)) {
        // Insert new block before
        newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock);

        // Adjust caret position if HR
        containerAndSiblingName(parentBlock, 'HR') ? moveToCaretPosition(newBlock) : moveToCaretPosition(parentBlock);
      } else {
        // Extract after fragment and insert it after the current block
        tmpRng = includeZwspInRange(rng).cloneRange();
        tmpRng.setEndAfter(parentBlock);
        fragment = tmpRng.extractContents();
        trimLeadingLineBreaks(fragment);
        newBlock = fragment.firstChild;
        dom.insertAfter(fragment, parentBlock);
        trimInlineElementsOnLeftSideOfBlock(dom, nonEmptyElementsMap, newBlock);
        addBrToBlockIfNeeded(dom, parentBlock);

        if (dom.isEmpty(parentBlock)) {
          emptyBlock(parentBlock);
        }

        newBlock.normalize();

        // New block might become empty if it's <p><b>a |</b></p>
        if (dom.isEmpty(newBlock)) {
          dom.remove(newBlock);
          insertNewBlockAfter();
        } else {
          moveToCaretPosition(newBlock);
        }
      }

      dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique

      // Allow custom handling of new blocks
      editor.fire('NewBlock', { newBlock: newBlock });
    };

    return {
      insert: insert
    };
  }
);

/**
 * EnterKey.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
 */

define(
  'tinymce.core.keyboard.EnterKey',
  [
    'tinymce.core.keyboard.InsertNewLine',
    'tinymce.core.util.VK'
  ],
  function (InsertNewLine, VK) {
    var endTypingLevel = function (undoManager) {
      if (undoManager.typing) {
        undoManager.typing = false;
        undoManager.add();
      }
    };

    var handleEnterKeyEvent = function (editor, event) {
      if (event.isDefaultPrevented()) {
        return;
      }

      event.preventDefault();

      endTypingLevel(editor.undoManager);
      editor.undoManager.transact(function () {
        if (editor.selection.isCollapsed() === false) {
          editor.execCommand('Delete');
        }

        InsertNewLine.insert(editor, event);
      });
    };

    var setup = function (editor) {
      editor.on('keydown', function (event) {
        if (event.keyCode === VK.ENTER) {
          handleEnterKeyEvent(editor, event);
        }
      });
    };

    return {
      setup: setup
    };
  }
);

/**
 * InsertSpace.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
 */

define(
  'tinymce.core.keyboard.InsertSpace',
  [
    'ephox.katamari.api.Fun',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.dom.NodeType',
    'tinymce.core.keyboard.BoundaryLocation',
    'tinymce.core.keyboard.InlineUtils'
  ],
  function (Fun, CaretPosition, NodeType, BoundaryLocation, InlineUtils) {
    var isValidInsertPoint = function (location, caretPosition) {
      return isAtStartOrEnd(location) && NodeType.isText(caretPosition.container());
    };

    var insertNbspAtPosition = function (editor, caretPosition) {
      var container = caretPosition.container();
      var offset = caretPosition.offset();

      container.insertData(offset, '\u00a0');
      editor.selection.setCursorLocation(container, offset + 1);
    };

    var insertAtLocation = function (editor, caretPosition, location) {
      if (isValidInsertPoint(location, caretPosition)) {
        insertNbspAtPosition(editor, caretPosition);
        return true;
      } else {
        return false;
      }
    };

    var insertAtCaret = function (editor) {
      var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor);
      var caretPosition = CaretPosition.fromRangeStart(editor.selection.getRng());
      var boundaryLocation = BoundaryLocation.readLocation(isInlineTarget, editor.getBody(), caretPosition);
      return boundaryLocation.map(Fun.curry(insertAtLocation, editor, caretPosition)).getOr(false);
    };

    var isAtStartOrEnd = function (location) {
      return location.fold(
        Fun.constant(false), // Before
        Fun.constant(true),  // Start
        Fun.constant(true),  // End
        Fun.constant(false)  // After
      );
    };

    var insertAtSelection = function (editor) {
      return editor.selection.isCollapsed() ? insertAtCaret(editor) : false;
    };

    return {
      insertAtSelection: insertAtSelection
    };
  }
);

/**
 * SpaceKey.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
 */

define(
  'tinymce.core.keyboard.SpaceKey',
  [
    'tinymce.core.keyboard.InsertSpace',
    'tinymce.core.keyboard.MatchKeys',
    'tinymce.core.util.VK'
  ],
  function (InsertSpace, MatchKeys, VK) {
    var executeKeydownOverride = function (editor, evt) {
      MatchKeys.execute([
        { keyCode: VK.SPACEBAR, action: MatchKeys.action(InsertSpace.insertAtSelection, editor) }
      ], evt).each(function (_) {
        evt.preventDefault();
      });
    };

    var setup = function (editor) {
      editor.on('keydown', function (evt) {
        if (evt.isDefaultPrevented() === false) {
          executeKeydownOverride(editor, evt);
        }
      });
    };

    return {
      setup: setup
    };
  }
);

/**
 * KeyboardOverrides.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
 */

define(
  'tinymce.core.keyboard.KeyboardOverrides',
  [
    'tinymce.core.keyboard.ArrowKeys',
    'tinymce.core.keyboard.BoundarySelection',
    'tinymce.core.keyboard.DeleteBackspaceKeys',
    'tinymce.core.keyboard.EnterKey',
    'tinymce.core.keyboard.SpaceKey'
  ],
  function (ArrowKeys, BoundarySelection, DeleteBackspaceKeys, EnterKey, SpaceKey) {
    var setup = function (editor) {
      var caret = BoundarySelection.setupSelectedState(editor);

      ArrowKeys.setup(editor, caret);
      DeleteBackspaceKeys.setup(editor, caret);
      EnterKey.setup(editor);
      SpaceKey.setup(editor);
    };

    return {
      setup: setup
    };
  }
);
/**
 * NodeChange.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
 */

/**
 * This class handles the nodechange event dispatching both manual and through selection change events.
 *
 * @class tinymce.NodeChange
 * @private
 */
define(
  'tinymce.core.NodeChange',
  [
    "tinymce.core.dom.RangeUtils",
    "tinymce.core.Env",
    "tinymce.core.util.Delay"
  ],
  function (RangeUtils, Env, Delay) {
    return function (editor) {
      var lastRng, lastPath = [];

      /**
       * Returns true/false if the current element path has been changed or not.
       *
       * @private
       * @return {Boolean} True if the element path is the same false if it's not.
       */
      var isSameElementPath = function (startElm) {
        var i, currentPath;

        currentPath = editor.$(startElm).parentsUntil(editor.getBody()).add(startElm);
        if (currentPath.length === lastPath.length) {
          for (i = currentPath.length; i >= 0; i--) {
            if (currentPath[i] !== lastPath[i]) {
              break;
            }
          }

          if (i === -1) {
            lastPath = currentPath;
            return true;
          }
        }

        lastPath = currentPath;

        return false;
      };

      // Gecko doesn't support the "selectionchange" event
      if (!('onselectionchange' in editor.getDoc())) {
        editor.on('NodeChange Click MouseUp KeyUp Focus', function (e) {
          var nativeRng, fakeRng;

          // Since DOM Ranges mutate on modification
          // of the DOM we need to clone it's contents
          nativeRng = editor.selection.getRng();
          fakeRng = {
            startContainer: nativeRng.startContainer,
            startOffset: nativeRng.startOffset,
            endContainer: nativeRng.endContainer,
            endOffset: nativeRng.endOffset
          };

          // Always treat nodechange as a selectionchange since applying
          // formatting to the current range wouldn't update the range but it's parent
          if (e.type == 'nodechange' || !RangeUtils.compareRanges(fakeRng, lastRng)) {
            editor.fire('SelectionChange');
          }

          lastRng = fakeRng;
        });
      }

      // IE has a bug where it fires a selectionchange on right click that has a range at the start of the body
      // When the contextmenu event fires the selection is located at the right location
      editor.on('contextmenu', function () {
        editor.fire('SelectionChange');
      });

      // Selection change is delayed ~200ms on IE when you click inside the current range
      editor.on('SelectionChange', function () {
        var startElm = editor.selection.getStart(true);

        // When focusout from after cef element to other input element the startelm can be undefined.
        // IE 8 will fire a selectionchange event with an incorrect selection
        // when focusing out of table cells. Click inside cell -> toolbar = Invalid SelectionChange event
        if (!startElm || (!Env.range && editor.selection.isCollapsed())) {
          return;
        }

        if (!isSameElementPath(startElm) && editor.dom.isChildOf(startElm, editor.getBody())) {
          editor.nodeChanged({ selectionChange: true });
        }
      });

      // Fire an extra nodeChange on mouseup for compatibility reasons
      editor.on('MouseUp', function (e) {
        if (!e.isDefaultPrevented()) {
          // Delay nodeChanged call for WebKit edge case issue where the range
          // isn't updated until after you click outside a selected image
          if (editor.selection.getNode().nodeName == 'IMG') {
            Delay.setEditorTimeout(editor, function () {
              editor.nodeChanged();
            });
          } else {
            editor.nodeChanged();
          }
        }
      });

      /**
       * Dispatches out a onNodeChange event to all observers. This method should be called when you
       * need to update the UI states or element path etc.
       *
       * @method nodeChanged
       * @param {Object} args Optional args to pass to NodeChange event handlers.
       */
      this.nodeChanged = function (args) {
        var selection = editor.selection, node, parents, root;

        // Fix for bug #1896577 it seems that this can not be fired while the editor is loading
        if (editor.initialized && selection && !editor.settings.disable_nodechange && !editor.readonly) {
          // Get start node
          root = editor.getBody();
          node = selection.getStart(true) || root;

          // Make sure the node is within the editor root or is the editor root
          if (node.ownerDocument != editor.getDoc() || !editor.dom.isChildOf(node, root)) {
            node = root;
          }

          // Get parents and add them to object
          parents = [];
          editor.dom.getParent(node, function (node) {
            if (node === root) {
              return true;
            }

            parents.push(node);
          });

          args = args || {};
          args.element = node;
          args.parents = parents;

          editor.fire('NodeChange', args);
        }
      };
    };
  }
);

/**
 * MousePosition.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
 */

/**
 * This module calculates an absolute coordinate inside the editor body for both local and global mouse events.
 *
 * @private
 * @class tinymce.dom.MousePosition
 */
define(
  'tinymce.core.dom.MousePosition',
  [
  ],
  function () {
    var getAbsolutePosition = function (elm) {
      var doc, docElem, win, clientRect;

      clientRect = elm.getBoundingClientRect();
      doc = elm.ownerDocument;
      docElem = doc.documentElement;
      win = doc.defaultView;

      return {
        top: clientRect.top + win.pageYOffset - docElem.clientTop,
        left: clientRect.left + win.pageXOffset - docElem.clientLeft
      };
    };

    var getBodyPosition = function (editor) {
      return editor.inline ? getAbsolutePosition(editor.getBody()) : { left: 0, top: 0 };
    };

    var getScrollPosition = function (editor) {
      var body = editor.getBody();
      return editor.inline ? { left: body.scrollLeft, top: body.scrollTop } : { left: 0, top: 0 };
    };

    var getBodyScroll = function (editor) {
      var body = editor.getBody(), docElm = editor.getDoc().documentElement;
      var inlineScroll = { left: body.scrollLeft, top: body.scrollTop };
      var iframeScroll = { left: body.scrollLeft || docElm.scrollLeft, top: body.scrollTop || docElm.scrollTop };

      return editor.inline ? inlineScroll : iframeScroll;
    };

    var getMousePosition = function (editor, event) {
      if (event.target.ownerDocument !== editor.getDoc()) {
        var iframePosition = getAbsolutePosition(editor.getContentAreaContainer());
        var scrollPosition = getBodyScroll(editor);

        return {
          left: event.pageX - iframePosition.left + scrollPosition.left,
          top: event.pageY - iframePosition.top + scrollPosition.top
        };
      }

      return {
        left: event.pageX,
        top: event.pageY
      };
    };

    var calculatePosition = function (bodyPosition, scrollPosition, mousePosition) {
      return {
        pageX: (mousePosition.left - bodyPosition.left) + scrollPosition.left,
        pageY: (mousePosition.top - bodyPosition.top) + scrollPosition.top
      };
    };

    var calc = function (editor, event) {
      return calculatePosition(getBodyPosition(editor), getScrollPosition(editor), getMousePosition(editor, event));
    };

    return {
      calc: calc
    };
  }
);

/**
 * DragDropOverrides.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
 */

/**
 * This module contains logic overriding the drag/drop logic of the editor.
 *
 * @private
 * @class tinymce.DragDropOverrides
 */
define(
  'tinymce.core.DragDropOverrides',
  [
    'global!document',
    'tinymce.core.dom.DOMUtils',
    'tinymce.core.dom.MousePosition',
    'tinymce.core.dom.NodeType',
    'tinymce.core.util.Arr',
    'tinymce.core.util.Delay',
    'tinymce.core.util.Fun'
  ],
  function (document, DOMUtils, MousePosition, NodeType, Arr, Delay, Fun) {
    var isContentEditableFalse = NodeType.isContentEditableFalse,
      isContentEditableTrue = NodeType.isContentEditableTrue;

    var isDraggable = function (rootElm, elm) {
      return isContentEditableFalse(elm) && elm !== rootElm;
    };

    var isValidDropTarget = function (editor, targetElement, dragElement) {
      if (targetElement === dragElement || editor.dom.isChildOf(targetElement, dragElement)) {
        return false;
      }

      if (isContentEditableFalse(targetElement)) {
        return false;
      }

      return true;
    };

    var cloneElement = function (elm) {
      var cloneElm = elm.cloneNode(true);
      cloneElm.removeAttribute('data-mce-selected');
      return cloneElm;
    };

    var createGhost = function (editor, elm, width, height) {
      var clonedElm = elm.cloneNode(true);

      editor.dom.setStyles(clonedElm, { width: width, height: height });
      editor.dom.setAttrib(clonedElm, 'data-mce-selected', null);

      var ghostElm = editor.dom.create('div', {
        'class': 'mce-drag-container',
        'data-mce-bogus': 'all',
        unselectable: 'on',
        contenteditable: 'false'
      });

      editor.dom.setStyles(ghostElm, {
        position: 'absolute',
        opacity: 0.5,
        overflow: 'hidden',
        border: 0,
        padding: 0,
        margin: 0,
        width: width,
        height: height
      });

      editor.dom.setStyles(clonedElm, {
        margin: 0,
        boxSizing: 'border-box'
      });

      ghostElm.appendChild(clonedElm);

      return ghostElm;
    };

    var appendGhostToBody = function (ghostElm, bodyElm) {
      if (ghostElm.parentNode !== bodyElm) {
        bodyElm.appendChild(ghostElm);
      }
    };

    var moveGhost = function (ghostElm, position, width, height, maxX, maxY) {
      var overflowX = 0, overflowY = 0;

      ghostElm.style.left = position.pageX + 'px';
      ghostElm.style.top = position.pageY + 'px';

      if (position.pageX + width > maxX) {
        overflowX = (position.pageX + width) - maxX;
      }

      if (position.pageY + height > maxY) {
        overflowY = (position.pageY + height) - maxY;
      }

      ghostElm.style.width = (width - overflowX) + 'px';
      ghostElm.style.height = (height - overflowY) + 'px';
    };

    var removeElement = function (elm) {
      if (elm && elm.parentNode) {
        elm.parentNode.removeChild(elm);
      }
    };

    var isLeftMouseButtonPressed = function (e) {
      return e.button === 0;
    };

    var hasDraggableElement = function (state) {
      return state.element;
    };

    var applyRelPos = function (state, position) {
      return {
        pageX: position.pageX - state.relX,
        pageY: position.pageY + 5
      };
    };

    var start = function (state, editor) {
      return function (e) {
        if (isLeftMouseButtonPressed(e)) {
          var ceElm = Arr.find(editor.dom.getParents(e.target), Fun.or(isContentEditableFalse, isContentEditableTrue));

          if (isDraggable(editor.getBody(), ceElm)) {
            var elmPos = editor.dom.getPos(ceElm);
            var bodyElm = editor.getBody();
            var docElm = editor.getDoc().documentElement;

            state.element = ceElm;
            state.screenX = e.screenX;
            state.screenY = e.screenY;
            state.maxX = (editor.inline ? bodyElm.scrollWidth : docElm.offsetWidth) - 2;
            state.maxY = (editor.inline ? bodyElm.scrollHeight : docElm.offsetHeight) - 2;
            state.relX = e.pageX - elmPos.x;
            state.relY = e.pageY - elmPos.y;
            state.width = ceElm.offsetWidth;
            state.height = ceElm.offsetHeight;
            state.ghost = createGhost(editor, ceElm, state.width, state.height);
          }
        }
      };
    };

    var move = function (state, editor) {
      // Reduces laggy drag behavior on Gecko
      var throttledPlaceCaretAt = Delay.throttle(function (clientX, clientY) {
        editor._selectionOverrides.hideFakeCaret();
        editor.selection.placeCaretAt(clientX, clientY);
      }, 0);

      return function (e) {
        var movement = Math.max(Math.abs(e.screenX - state.screenX), Math.abs(e.screenY - state.screenY));

        if (hasDraggableElement(state) && !state.dragging && movement > 10) {
          var args = editor.fire('dragstart', { target: state.element });
          if (args.isDefaultPrevented()) {
            return;
          }

          state.dragging = true;
          editor.focus();
        }

        if (state.dragging) {
          var targetPos = applyRelPos(state, MousePosition.calc(editor, e));

          appendGhostToBody(state.ghost, editor.getBody());
          moveGhost(state.ghost, targetPos, state.width, state.height, state.maxX, state.maxY);

          throttledPlaceCaretAt(e.clientX, e.clientY);
        }
      };
    };

    // Returns the raw element instead of the fake cE=false element
    var getRawTarget = function (selection) {
      var rng = selection.getSel().getRangeAt(0);
      var startContainer = rng.startContainer;
      return startContainer.nodeType === 3 ? startContainer.parentNode : startContainer;
    };

    var drop = function (state, editor) {
      return function (e) {
        if (state.dragging) {
          if (isValidDropTarget(editor, getRawTarget(editor.selection), state.element)) {
            var targetClone = cloneElement(state.element);

            var args = editor.fire('drop', {
              targetClone: targetClone,
              clientX: e.clientX,
              clientY: e.clientY
            });

            if (!args.isDefaultPrevented()) {
              targetClone = args.targetClone;

              editor.undoManager.transact(function () {
                removeElement(state.element);
                editor.insertContent(editor.dom.getOuterHTML(targetClone));
                editor._selectionOverrides.hideFakeCaret();
              });
            }
          }
        }

        removeDragState(state);
      };
    };

    var stop = function (state, editor) {
      return function () {
        removeDragState(state);
        if (state.dragging) {
          editor.fire('dragend');
        }
      };
    };

    var removeDragState = function (state) {
      state.dragging = false;
      state.element = null;
      removeElement(state.ghost);
    };

    var bindFakeDragEvents = function (editor) {
      var state = {}, pageDom, dragStartHandler, dragHandler, dropHandler, dragEndHandler, rootDocument;

      pageDom = DOMUtils.DOM;
      rootDocument = document;
      dragStartHandler = start(state, editor);
      dragHandler = move(state, editor);
      dropHandler = drop(state, editor);
      dragEndHandler = stop(state, editor);

      editor.on('mousedown', dragStartHandler);
      editor.on('mousemove', dragHandler);
      editor.on('mouseup', dropHandler);

      pageDom.bind(rootDocument, 'mousemove', dragHandler);
      pageDom.bind(rootDocument, 'mouseup', dragEndHandler);

      editor.on('remove', function () {
        pageDom.unbind(rootDocument, 'mousemove', dragHandler);
        pageDom.unbind(rootDocument, 'mouseup', dragEndHandler);
      });
    };

    var blockIeDrop = function (editor) {
      editor.on('drop', function (e) {
        // FF doesn't pass out clientX/clientY for drop since this is for IE we just use null instead
        var realTarget = typeof e.clientX !== 'undefined' ? editor.getDoc().elementFromPoint(e.clientX, e.clientY) : null;

        if (isContentEditableFalse(realTarget) || isContentEditableFalse(editor.dom.getContentEditableParent(realTarget))) {
          e.preventDefault();
        }
      });
    };

    var init = function (editor) {
      bindFakeDragEvents(editor);
      blockIeDrop(editor);
    };

    return {
      init: init
    };
  }
);

/**
 * FakeCaret.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
 */

/**
 * This module contains logic for rendering a fake visual caret.
 *
 * @private
 * @class tinymce.caret.FakeCaret
 */
define(
  'tinymce.core.caret.FakeCaret',
  [
    'global!clearInterval',
    'tinymce.core.caret.CaretContainer',
    'tinymce.core.caret.CaretContainerRemove',
    'tinymce.core.dom.DomQuery',
    'tinymce.core.dom.NodeType',
    'tinymce.core.geom.ClientRect',
    'tinymce.core.util.Delay'
  ],
  function (clearInterval, CaretContainer, CaretContainerRemove, DomQuery, NodeType, ClientRect, Delay) {
    var isContentEditableFalse = NodeType.isContentEditableFalse;

    var isTableCell = function (node) {
      return node && /^(TD|TH)$/i.test(node.nodeName);
    };

    return function (rootNode, isBlock) {
      var cursorInterval, $lastVisualCaret = null, caretContainerNode;

      var getAbsoluteClientRect = function (node, before) {
        var clientRect = ClientRect.collapse(node.getBoundingClientRect(), before),
          docElm, scrollX, scrollY, margin, rootRect;

        if (rootNode.tagName == 'BODY') {
          docElm = rootNode.ownerDocument.documentElement;
          scrollX = rootNode.scrollLeft || docElm.scrollLeft;
          scrollY = rootNode.scrollTop || docElm.scrollTop;
        } else {
          rootRect = rootNode.getBoundingClientRect();
          scrollX = rootNode.scrollLeft - rootRect.left;
          scrollY = rootNode.scrollTop - rootRect.top;
        }

        clientRect.left += scrollX;
        clientRect.right += scrollX;
        clientRect.top += scrollY;
        clientRect.bottom += scrollY;
        clientRect.width = 1;

        margin = node.offsetWidth - node.clientWidth;

        if (margin > 0) {
          if (before) {
            margin *= -1;
          }

          clientRect.left += margin;
          clientRect.right += margin;
        }

        return clientRect;
      };

      var trimInlineCaretContainers = function () {
        var contentEditableFalseNodes, node, sibling, i, data;

        contentEditableFalseNodes = DomQuery('*[contentEditable=false]', rootNode);
        for (i = 0; i < contentEditableFalseNodes.length; i++) {
          node = contentEditableFalseNodes[i];

          sibling = node.previousSibling;
          if (CaretContainer.endsWithCaretContainer(sibling)) {
            data = sibling.data;

            if (data.length == 1) {
              sibling.parentNode.removeChild(sibling);
            } else {
              sibling.deleteData(data.length - 1, 1);
            }
          }

          sibling = node.nextSibling;
          if (CaretContainer.startsWithCaretContainer(sibling)) {
            data = sibling.data;

            if (data.length == 1) {
              sibling.parentNode.removeChild(sibling);
            } else {
              sibling.deleteData(0, 1);
            }
          }
        }

        return null;
      };

      var show = function (before, node) {
        var clientRect, rng;

        hide();

        if (isTableCell(node)) {
          return null;
        }

        if (isBlock(node)) {
          caretContainerNode = CaretContainer.insertBlock('p', node, before);
          clientRect = getAbsoluteClientRect(node, before);
          DomQuery(caretContainerNode).css('top', clientRect.top);

          $lastVisualCaret = DomQuery('<div class="mce-visual-caret" data-mce-bogus="all"></div>').css(clientRect).appendTo(rootNode);

          if (before) {
            $lastVisualCaret.addClass('mce-visual-caret-before');
          }

          startBlink();

          rng = node.ownerDocument.createRange();
          rng.setStart(caretContainerNode, 0);
          rng.setEnd(caretContainerNode, 0);
        } else {
          caretContainerNode = CaretContainer.insertInline(node, before);
          rng = node.ownerDocument.createRange();

          if (isContentEditableFalse(caretContainerNode.nextSibling)) {
            rng.setStart(caretContainerNode, 0);
            rng.setEnd(caretContainerNode, 0);
          } else {
            rng.setStart(caretContainerNode, 1);
            rng.setEnd(caretContainerNode, 1);
          }

          return rng;
        }

        return rng;
      };

      var hide = function () {
        trimInlineCaretContainers();

        if (caretContainerNode) {
          CaretContainerRemove.remove(caretContainerNode);
          caretContainerNode = null;
        }

        if ($lastVisualCaret) {
          $lastVisualCaret.remove();
          $lastVisualCaret = null;
        }

        clearInterval(cursorInterval);
      };

      var hasFocus = function () {
        return rootNode.ownerDocument.activeElement === rootNode;
      };

      var startBlink = function () {
        cursorInterval = Delay.setInterval(function () {
          if (hasFocus()) {
            DomQuery('div.mce-visual-caret', rootNode).toggleClass('mce-visual-caret-hidden');
          } else {
            DomQuery('div.mce-visual-caret', rootNode).addClass('mce-visual-caret-hidden');
          }
        }, 500);
      };

      var destroy = function () {
        Delay.clearInterval(cursorInterval);
      };

      var getCss = function () {
        return (
          '.mce-visual-caret {' +
          'position: absolute;' +
          'background-color: black;' +
          'background-color: currentcolor;' +
          '}' +
          '.mce-visual-caret-hidden {' +
          'display: none;' +
          '}' +
          '*[data-mce-caret] {' +
          'position: absolute;' +
          'left: -1000px;' +
          'right: auto;' +
          'top: 0;' +
          'margin: 0;' +
          'padding: 0;' +
          '}'
        );
      };

      return {
        show: show,
        hide: hide,
        getCss: getCss,
        destroy: destroy
      };
    };
  }
);
/**
 * SelectionOverrides.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
 */

define(
  'tinymce.core.SelectionOverrides',
  [
    'ephox.katamari.api.Arr',
    'ephox.sugar.api.dom.Remove',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.properties.Attr',
    'ephox.sugar.api.search.SelectorFilter',
    'ephox.sugar.api.search.SelectorFind',
    'tinymce.core.DragDropOverrides',
    'tinymce.core.EditorView',
    'tinymce.core.Env',
    'tinymce.core.caret.CaretContainer',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.caret.CaretUtils',
    'tinymce.core.caret.CaretWalker',
    'tinymce.core.caret.FakeCaret',
    'tinymce.core.caret.LineUtils',
    'tinymce.core.dom.ElementType',
    'tinymce.core.dom.NodeType',
    'tinymce.core.dom.RangePoint',
    'tinymce.core.keyboard.CefUtils',
    'tinymce.core.util.Delay',
    'tinymce.core.util.VK'
  ],
  function (
    Arr, Remove, Element, Attr, SelectorFilter, SelectorFind, DragDropOverrides, EditorView, Env, CaretContainer, CaretPosition, CaretUtils, CaretWalker, FakeCaret,
    LineUtils, ElementType, NodeType, RangePoint, CefUtils, Delay, VK
  ) {
    var isContentEditableTrue = NodeType.isContentEditableTrue,
      isContentEditableFalse = NodeType.isContentEditableFalse,
      isAfterContentEditableFalse = CaretUtils.isAfterContentEditableFalse,
      isBeforeContentEditableFalse = CaretUtils.isBeforeContentEditableFalse;

    var SelectionOverrides = function (editor) {
      var isBlock = function (node) {
        return editor.dom.isBlock(node);
      };

      var rootNode = editor.getBody();
      var fakeCaret = new FakeCaret(editor.getBody(), isBlock),
        realSelectionId = 'sel-' + editor.dom.uniqueId(),
        selectedContentEditableNode;

      var isFakeSelectionElement = function (elm) {
        return editor.dom.hasClass(elm, 'mce-offscreen-selection');
      };

      var getRealSelectionElement = function () {
        var container = editor.dom.get(realSelectionId);
        return container ? container.getElementsByTagName('*')[0] : container;
      };

      var setRange = function (range) {
        //console.log('setRange', range);
        if (range) {
          editor.selection.setRng(range);
        }
      };

      var getRange = function () {
        return editor.selection.getRng();
      };

      var scrollIntoView = function (node, alignToTop) {
        editor.selection.scrollIntoView(node, alignToTop);
      };

      var showCaret = function (direction, node, before) {
        var e;

        e = editor.fire('ShowCaret', {
          target: node,
          direction: direction,
          before: before
        });

        if (e.isDefaultPrevented()) {
          return null;
        }

        scrollIntoView(node, direction === -1);

        return fakeCaret.show(before, node);
      };

      var getNormalizedRangeEndPoint = function (direction, range) {
        range = CaretUtils.normalizeRange(direction, rootNode, range);

        if (direction == -1) {
          return CaretPosition.fromRangeStart(range);
        }

        return CaretPosition.fromRangeEnd(range);
      };

      var showBlockCaretContainer = function (blockCaretContainer) {
        if (blockCaretContainer.hasAttribute('data-mce-caret')) {
          CaretContainer.showCaretContainerBlock(blockCaretContainer);
          setRange(getRange()); // Removes control rect on IE
          scrollIntoView(blockCaretContainer[0]);
        }
      };

      var registerEvents = function () {
        var getContentEditableRoot = function (node) {
          var root = editor.getBody();

          while (node && node != root) {
            if (isContentEditableTrue(node) || isContentEditableFalse(node)) {
              return node;
            }

            node = node.parentNode;
          }

          return null;
        };

        // Some browsers (Chrome) lets you place the caret after a cE=false
        // Make sure we render the caret container in this case
        editor.on('mouseup', function (e) {
          var range = getRange();

          if (range.collapsed && EditorView.isXYInContentArea(editor, e.clientX, e.clientY)) {
            setRange(CefUtils.renderCaretAtRange(editor, range));
          }
        });

        editor.on('click', function (e) {
          var contentEditableRoot;

          contentEditableRoot = getContentEditableRoot(e.target);
          if (contentEditableRoot) {
            // Prevent clicks on links in a cE=false element
            if (isContentEditableFalse(contentEditableRoot)) {
              e.preventDefault();
              editor.focus();
            }

            // Removes fake selection if a cE=true is clicked within a cE=false like the toc title
            if (isContentEditableTrue(contentEditableRoot)) {
              if (editor.dom.isChildOf(contentEditableRoot, editor.selection.getNode())) {
                removeContentEditableSelection();
              }
            }
          }
        });

        editor.on('blur NewBlock', function () {
          removeContentEditableSelection();
        });

        var handleTouchSelect = function (editor) {
          var moved = false;

          editor.on('touchstart', function () {
            moved = false;
          });

          editor.on('touchmove', function () {
            moved = true;
          });

          editor.on('touchend', function (e) {
            var contentEditableRoot = getContentEditableRoot(e.target);

            if (isContentEditableFalse(contentEditableRoot)) {
              if (!moved) {
                e.preventDefault();
                setContentEditableSelection(CefUtils.selectNode(editor, contentEditableRoot));
              }
            }
          });
        };

        var hasNormalCaretPosition = function (elm) {
          var caretWalker = new CaretWalker(elm);

          if (!elm.firstChild) {
            return false;
          }

          var startPos = CaretPosition.before(elm.firstChild);
          var newPos = caretWalker.next(startPos);

          return newPos && !isBeforeContentEditableFalse(newPos) && !isAfterContentEditableFalse(newPos);
        };

        var isInSameBlock = function (node1, node2) {
          var block1 = editor.dom.getParent(node1, editor.dom.isBlock);
          var block2 = editor.dom.getParent(node2, editor.dom.isBlock);
          return block1 === block2;
        };

        // Checks if the target node is in a block and if that block has a caret position better than the
        // suggested caretNode this is to prevent the caret from being sucked in towards a cE=false block if
        // they are adjacent on the vertical axis
        var hasBetterMouseTarget = function (targetNode, caretNode) {
          var targetBlock = editor.dom.getParent(targetNode, editor.dom.isBlock);
          var caretBlock = editor.dom.getParent(caretNode, editor.dom.isBlock);

          return targetBlock && !isInSameBlock(targetBlock, caretBlock) && hasNormalCaretPosition(targetBlock);
        };

        handleTouchSelect(editor);

        editor.on('mousedown', function (e) {
          var contentEditableRoot;

          if (EditorView.isXYInContentArea(editor, e.clientX, e.clientY) === false) {
            return;
          }

          contentEditableRoot = getContentEditableRoot(e.target);
          if (contentEditableRoot) {
            if (isContentEditableFalse(contentEditableRoot)) {
              e.preventDefault();
              setContentEditableSelection(CefUtils.selectNode(editor, contentEditableRoot));
            } else {
              removeContentEditableSelection();

              // Check that we're not attempting a shift + click select within a contenteditable='true' element
              if (!(isContentEditableTrue(contentEditableRoot) && e.shiftKey) && !RangePoint.isXYWithinRange(e.clientX, e.clientY, editor.selection.getRng())) {
                ElementType.isVoid(Element.fromDom(e.target)) ? editor.selection.select(e.target) : editor.selection.placeCaretAt(e.clientX, e.clientY);
              }
            }
          } else {
            // Remove needs to be called here since the mousedown might alter the selection without calling selection.setRng
            // and therefore not fire the AfterSetSelectionRange event.
            removeContentEditableSelection();
            hideFakeCaret();

            var caretInfo = LineUtils.closestCaret(rootNode, e.clientX, e.clientY);
            if (caretInfo) {
              if (!hasBetterMouseTarget(e.target, caretInfo.node)) {
                e.preventDefault();
                editor.getBody().focus();
                setRange(showCaret(1, caretInfo.node, caretInfo.before));
              }
            }
          }
        });

        editor.on('keypress', function (e) {
          if (VK.modifierPressed(e)) {
            return;
          }

          switch (e.keyCode) {
            default:
              if (isContentEditableFalse(editor.selection.getNode())) {
                e.preventDefault();
              }
              break;
          }
        });

        editor.on('getSelectionRange', function (e) {
          var rng = e.range;

          if (selectedContentEditableNode) {
            if (!selectedContentEditableNode.parentNode) {
              selectedContentEditableNode = null;
              return;
            }

            rng = rng.cloneRange();
            rng.selectNode(selectedContentEditableNode);
            e.range = rng;
          }
        });

        editor.on('setSelectionRange', function (e) {
          var rng;

          rng = setContentEditableSelection(e.range, e.forward);
          if (rng) {
            e.range = rng;
          }
        });

        editor.on('AfterSetSelectionRange', function (e) {
          var rng = e.range;

          if (!isRangeInCaretContainer(rng)) {
            hideFakeCaret();
          }

          if (!isFakeSelectionElement(rng.startContainer.parentNode)) {
            removeContentEditableSelection();
          }
        });

        editor.on('focus', function () {
          // Make sure we have a proper fake caret on focus
          Delay.setEditorTimeout(editor, function () {
            editor.selection.setRng(CefUtils.renderRangeCaret(editor, editor.selection.getRng()));
          }, 0);
        });

        editor.on('copy', function (e) {
          var clipboardData = e.clipboardData;

          // Make sure we get proper html/text for the fake cE=false selection
          // Doesn't work at all on Edge since it doesn't have proper clipboardData support
          if (!e.isDefaultPrevented() && e.clipboardData && !Env.ie) {
            var realSelectionElement = getRealSelectionElement();
            if (realSelectionElement) {
              e.preventDefault();
              clipboardData.clearData();
              clipboardData.setData('text/html', realSelectionElement.outerHTML);
              clipboardData.setData('text/plain', realSelectionElement.outerText);
            }
          }
        });

        DragDropOverrides.init(editor);
      };

      var addCss = function () {
        var styles = editor.contentStyles, rootClass = '.mce-content-body';

        styles.push(fakeCaret.getCss());
        styles.push(
          rootClass + ' .mce-offscreen-selection {' +
          'position: absolute;' +
          'left: -9999999999px;' +
          'max-width: 1000000px;' +
          '}' +
          rootClass + ' *[contentEditable=false] {' +
          'cursor: default;' +
          '}' +
          rootClass + ' *[contentEditable=true] {' +
          'cursor: text;' +
          '}'
        );
      };

      var isWithinCaretContainer = function (node) {
        return (
          CaretContainer.isCaretContainer(node) ||
          CaretContainer.startsWithCaretContainer(node) ||
          CaretContainer.endsWithCaretContainer(node)
        );
      };

      var isRangeInCaretContainer = function (rng) {
        return isWithinCaretContainer(rng.startContainer) || isWithinCaretContainer(rng.endContainer);
      };

      var setContentEditableSelection = function (range, forward) {
        var node, $ = editor.$, dom = editor.dom, $realSelectionContainer, sel,
          startContainer, startOffset, endOffset, e, caretPosition, targetClone, origTargetClone;

        if (!range) {
          return null;
        }

        if (range.collapsed) {
          if (!isRangeInCaretContainer(range)) {
            if (forward === false) {
              caretPosition = getNormalizedRangeEndPoint(-1, range);

              if (isContentEditableFalse(caretPosition.getNode(true))) {
                return showCaret(-1, caretPosition.getNode(true), false);
              }

              if (isContentEditableFalse(caretPosition.getNode())) {
                return showCaret(-1, caretPosition.getNode(), !caretPosition.isAtEnd());
              }
            } else {
              caretPosition = getNormalizedRangeEndPoint(1, range);

              if (isContentEditableFalse(caretPosition.getNode())) {
                return showCaret(1, caretPosition.getNode(), !caretPosition.isAtEnd());
              }

              if (isContentEditableFalse(caretPosition.getNode(true))) {
                return showCaret(1, caretPosition.getNode(true), false);
              }
            }
          }

          return null;
        }

        startContainer = range.startContainer;
        startOffset = range.startOffset;
        endOffset = range.endOffset;

        // Normalizes <span cE=false>[</span>] to [<span cE=false></span>]
        if (startContainer.nodeType == 3 && startOffset == 0 && isContentEditableFalse(startContainer.parentNode)) {
          startContainer = startContainer.parentNode;
          startOffset = dom.nodeIndex(startContainer);
          startContainer = startContainer.parentNode;
        }

        if (startContainer.nodeType != 1) {
          return null;
        }

        if (endOffset == startOffset + 1) {
          node = startContainer.childNodes[startOffset];
        }

        if (!isContentEditableFalse(node)) {
          return null;
        }

        targetClone = origTargetClone = node.cloneNode(true);
        e = editor.fire('ObjectSelected', { target: node, targetClone: targetClone });
        if (e.isDefaultPrevented()) {
          return null;
        }

        $realSelectionContainer = SelectorFind.descendant(Element.fromDom(editor.getBody()), '#' + realSelectionId).fold(
          function () {
            return $([]);
          },
          function (elm) {
            return $([elm.dom()]);
          }
        );

        targetClone = e.targetClone;
        if ($realSelectionContainer.length === 0) {
          $realSelectionContainer = $(
            '<div data-mce-bogus="all" class="mce-offscreen-selection"></div>'
          ).attr('id', realSelectionId);

          $realSelectionContainer.appendTo(editor.getBody());
        }

        range = editor.dom.createRng();

        // WHY is IE making things so hard! Copy on <i contentEditable="false">x</i> produces: <em>x</em>
        // This is a ridiculous hack where we place the selection from a block over the inline element
        // so that just the inline element is copied as is and not converted.
        if (targetClone === origTargetClone && Env.ie) {
          $realSelectionContainer.empty().append('<p style="font-size: 0" data-mce-bogus="all">\u00a0</p>').append(targetClone);
          range.setStartAfter($realSelectionContainer[0].firstChild.firstChild);
          range.setEndAfter(targetClone);
        } else {
          $realSelectionContainer.empty().append('\u00a0').append(targetClone).append('\u00a0');
          range.setStart($realSelectionContainer[0].firstChild, 1);
          range.setEnd($realSelectionContainer[0].lastChild, 0);
        }

        $realSelectionContainer.css({
          top: dom.getPos(node, editor.getBody()).y
        });

        $realSelectionContainer[0].focus();
        sel = editor.selection.getSel();
        sel.removeAllRanges();
        sel.addRange(range);

        Arr.each(SelectorFilter.descendants(Element.fromDom(editor.getBody()), '*[data-mce-selected]'), function (elm) {
          Attr.remove(elm, 'data-mce-selected');
        });

        node.setAttribute('data-mce-selected', 1);
        selectedContentEditableNode = node;
        hideFakeCaret();

        return range;
      };

      var removeContentEditableSelection = function () {
        if (selectedContentEditableNode) {
          selectedContentEditableNode.removeAttribute('data-mce-selected');
          SelectorFind.descendant(Element.fromDom(editor.getBody()), '#' + realSelectionId).each(Remove.remove);
          selectedContentEditableNode = null;
        }
      };

      var destroy = function () {
        fakeCaret.destroy();
        selectedContentEditableNode = null;
      };

      var hideFakeCaret = function () {
        fakeCaret.hide();
      };

      if (Env.ceFalse) {
        registerEvents();
        addCss();
      }

      return {
        showCaret: showCaret,
        showBlockCaretContainer: showBlockCaretContainer,
        hideFakeCaret: hideFakeCaret,
        destroy: destroy
      };
    };

    return SelectionOverrides;
  }
);

/**
 * Diff.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
 */

/**
 * JS Implementation of the O(ND) Difference Algorithm by Eugene W. Myers.
 *
 * @class tinymce.undo.Diff
 * @private
 */
define(
  'tinymce.core.undo.Diff',
  [
  ],
  function () {
    var KEEP = 0, INSERT = 1, DELETE = 2;

    var diff = function (left, right) {
      var size = left.length + right.length + 2;
      var vDown = new Array(size);
      var vUp = new Array(size);

      var snake = function (start, end, diag) {
        return {
          start: start,
          end: end,
          diag: diag
        };
      };

      var buildScript = function (start1, end1, start2, end2, script) {
        var middle = getMiddleSnake(start1, end1, start2, end2);

        if (middle === null || middle.start === end1 && middle.diag === end1 - end2 ||
          middle.end === start1 && middle.diag === start1 - start2) {
          var i = start1;
          var j = start2;
          while (i < end1 || j < end2) {
            if (i < end1 && j < end2 && left[i] === right[j]) {
              script.push([KEEP, left[i]]);
              ++i;
              ++j;
            } else {
              if (end1 - start1 > end2 - start2) {
                script.push([DELETE, left[i]]);
                ++i;
              } else {
                script.push([INSERT, right[j]]);
                ++j;
              }
            }
          }
        } else {
          buildScript(start1, middle.start, start2, middle.start - middle.diag, script);
          for (var i2 = middle.start; i2 < middle.end; ++i2) {
            script.push([KEEP, left[i2]]);
          }
          buildScript(middle.end, end1, middle.end - middle.diag, end2, script);
        }
      };

      var buildSnake = function (start, diag, end1, end2) {
        var end = start;
        while (end - diag < end2 && end < end1 && left[end] === right[end - diag]) {
          ++end;
        }
        return snake(start, end, diag);
      };

      var getMiddleSnake = function (start1, end1, start2, end2) {
        // Myers Algorithm
        // Initialisations
        var m = end1 - start1;
        var n = end2 - start2;
        if (m === 0 || n === 0) {
          return null;
        }

        var delta = m - n;
        var sum = n + m;
        var offset = (sum % 2 === 0 ? sum : sum + 1) / 2;
        vDown[1 + offset] = start1;
        vUp[1 + offset] = end1 + 1;

        for (var d = 0; d <= offset; ++d) {
          // Down
          for (var k = -d; k <= d; k += 2) {
            // First step

            var i = k + offset;
            if (k === -d || k != d && vDown[i - 1] < vDown[i + 1]) {
              vDown[i] = vDown[i + 1];
            } else {
              vDown[i] = vDown[i - 1] + 1;
            }

            var x = vDown[i];
            var y = x - start1 + start2 - k;

            while (x < end1 && y < end2 && left[x] === right[y]) {
              vDown[i] = ++x;
              ++y;
            }
            // Second step
            if (delta % 2 != 0 && delta - d <= k && k <= delta + d) {
              if (vUp[i - delta] <= vDown[i]) {
                return buildSnake(vUp[i - delta], k + start1 - start2, end1, end2);
              }
            }
          }

          // Up
          for (k = delta - d; k <= delta + d; k += 2) {
            // First step
            i = k + offset - delta;
            if (k === delta - d || k != delta + d && vUp[i + 1] <= vUp[i - 1]) {
              vUp[i] = vUp[i + 1] - 1;
            } else {
              vUp[i] = vUp[i - 1];
            }

            x = vUp[i] - 1;
            y = x - start1 + start2 - k;
            while (x >= start1 && y >= start2 && left[x] === right[y]) {
              vUp[i] = x--;
              y--;
            }
            // Second step
            if (delta % 2 === 0 && -d <= k && k <= d) {
              if (vUp[i] <= vDown[i + delta]) {
                return buildSnake(vUp[i], k + start1 - start2, end1, end2);
              }
            }
          }
        }
      };

      var script = [];
      buildScript(0, left.length, 0, right.length, script);
      return script;
    };

    return {
      KEEP: KEEP,
      DELETE: DELETE,
      INSERT: INSERT,
      diff: diff
    };
  }
);
/**
 * Fragments.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
 */

/**
 * This module reads and applies html fragments from/to dom nodes.
 *
 * @class tinymce.undo.Fragments
 * @private
 */
define(
  'tinymce.core.undo.Fragments',
  [
    'global!document',
    'tinymce.core.html.Entities',
    'tinymce.core.undo.Diff',
    'tinymce.core.util.Arr'
  ],
  function (document, Entities, Diff, Arr) {
    var getOuterHtml = function (elm) {
      if (elm.nodeType === 1) {
        return elm.outerHTML;
      } else if (elm.nodeType === 3) {
        return Entities.encodeRaw(elm.data, false);
      } else if (elm.nodeType === 8) {
        return '<!--' + elm.data + '-->';
      }

      return '';
    };

    var createFragment = function (html) {
      var frag, node, container;

      container = document.createElement("div");
      frag = document.createDocumentFragment();

      if (html) {
        container.innerHTML = html;
      }

      while ((node = container.firstChild)) {
        frag.appendChild(node);
      }

      return frag;
    };

    var insertAt = function (elm, html, index) {
      var fragment = createFragment(html);
      if (elm.hasChildNodes() && index < elm.childNodes.length) {
        var target = elm.childNodes[index];
        target.parentNode.insertBefore(fragment, target);
      } else {
        elm.appendChild(fragment);
      }
    };

    var removeAt = function (elm, index) {
      if (elm.hasChildNodes() && index < elm.childNodes.length) {
        var target = elm.childNodes[index];
        target.parentNode.removeChild(target);
      }
    };

    var applyDiff = function (diff, elm) {
      var index = 0;
      Arr.each(diff, function (action) {
        if (action[0] === Diff.KEEP) {
          index++;
        } else if (action[0] === Diff.INSERT) {
          insertAt(elm, action[1], index);
          index++;
        } else if (action[0] === Diff.DELETE) {
          removeAt(elm, index);
        }
      });
    };

    var read = function (elm) {
      return Arr.filter(Arr.map(elm.childNodes, getOuterHtml), function (item) {
        return item.length > 0;
      });
    };

    var write = function (fragments, elm) {
      var currentFragments = Arr.map(elm.childNodes, getOuterHtml);
      applyDiff(Diff.diff(currentFragments, fragments), elm);
      return elm;
    };

    return {
      read: read,
      write: write
    };
  }
);
/**
 * Levels.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
 */

/**
 * This module handles getting/setting undo levels to/from editor instances.
 *
 * @class tinymce.undo.Levels
 * @private
 */
define(
  'tinymce.core.undo.Levels',
  [
    "tinymce.core.util.Arr",
    "tinymce.core.undo.Fragments"
  ],
  function (Arr, Fragments) {
    var hasIframes = function (html) {
      return html.indexOf('</iframe>') !== -1;
    };

    var createFragmentedLevel = function (fragments) {
      return {
        type: 'fragmented',
        fragments: fragments,
        content: '',
        bookmark: null,
        beforeBookmark: null
      };
    };

    var createCompleteLevel = function (content) {
      return {
        type: 'complete',
        fragments: null,
        content: content,
        bookmark: null,
        beforeBookmark: null
      };
    };

    var createFromEditor = function (editor) {
      var fragments, content, trimmedFragments;

      fragments = Fragments.read(editor.getBody());
      trimmedFragments = Arr.map(fragments, function (html) {
        return editor.serializer.trimContent(html);
      });
      content = trimmedFragments.join('');

      return hasIframes(content) ? createFragmentedLevel(trimmedFragments) : createCompleteLevel(content);
    };

    var applyToEditor = function (editor, level, before) {
      if (level.type === 'fragmented') {
        Fragments.write(level.fragments, editor.getBody());
      } else {
        editor.setContent(level.content, { format: 'raw' });
      }

      editor.selection.moveToBookmark(before ? level.beforeBookmark : level.bookmark);
    };

    var getLevelContent = function (level) {
      return level.type === 'fragmented' ? level.fragments.join('') : level.content;
    };

    var isEq = function (level1, level2) {
      return !!level1 && !!level2 && getLevelContent(level1) === getLevelContent(level2);
    };

    return {
      createFragmentedLevel: createFragmentedLevel,
      createCompleteLevel: createCompleteLevel,
      createFromEditor: createFromEditor,
      applyToEditor: applyToEditor,
      isEq: isEq
    };
  }
);
/**
 * UndoManager.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
 */

/**
 * This class handles the undo/redo history levels for the editor. Since the built-in undo/redo has major drawbacks a custom one was needed.
 *
 * @class tinymce.UndoManager
 */
define(
  'tinymce.core.UndoManager',
  [
    "tinymce.core.util.VK",
    "tinymce.core.util.Tools",
    "tinymce.core.undo.Levels"
  ],
  function (VK, Tools, Levels) {
    return function (editor) {
      var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0;

      var isUnlocked = function () {
        return locks === 0;
      };

      var setTyping = function (typing) {
        if (isUnlocked()) {
          self.typing = typing;
        }
      };

      var setDirty = function (state) {
        editor.setDirty(state);
      };

      var addNonTypingUndoLevel = function (e) {
        setTyping(false);
        self.add({}, e);
      };

      var endTyping = function () {
        if (self.typing) {
          setTyping(false);
          self.add();
        }
      };

      // Add initial undo level when the editor is initialized
      editor.on('init', function () {
        self.add();
      });

      // Get position before an execCommand is processed
      editor.on('BeforeExecCommand', function (e) {
        var cmd = e.command;

        if (cmd !== 'Undo' && cmd !== 'Redo' && cmd !== 'mceRepaint') {
          endTyping();
          self.beforeChange();
        }
      });

      // Add undo level after an execCommand call was made
      editor.on('ExecCommand', function (e) {
        var cmd = e.command;

        if (cmd !== 'Undo' && cmd !== 'Redo' && cmd !== 'mceRepaint') {
          addNonTypingUndoLevel(e);
        }
      });

      editor.on('ObjectResizeStart Cut', function () {
        self.beforeChange();
      });

      editor.on('SaveContent ObjectResized blur', addNonTypingUndoLevel);
      editor.on('DragEnd', addNonTypingUndoLevel);

      editor.on('KeyUp', function (e) {
        var keyCode = e.keyCode;

        // If key is prevented then don't add undo level
        // This would happen on keyboard shortcuts for example
        if (e.isDefaultPrevented()) {
          return;
        }

        if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45 || e.ctrlKey) {
          addNonTypingUndoLevel();
          editor.nodeChanged();
        }

        if (keyCode === 46 || keyCode === 8) {
          editor.nodeChanged();
        }

        // Fire a TypingUndo/Change event on the first character entered
        if (isFirstTypedCharacter && self.typing && Levels.isEq(Levels.createFromEditor(editor), data[0]) === false) {
          if (editor.isDirty() === false) {
            setDirty(true);
            editor.fire('change', { level: data[0], lastLevel: null });
          }

          editor.fire('TypingUndo');
          isFirstTypedCharacter = false;
          editor.nodeChanged();
        }
      });

      editor.on('KeyDown', function (e) {
        var keyCode = e.keyCode;

        // If key is prevented then don't add undo level
        // This would happen on keyboard shortcuts for example
        if (e.isDefaultPrevented()) {
          return;
        }

        // Is character position keys left,right,up,down,home,end,pgdown,pgup,enter
        if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45) {
          if (self.typing) {
            addNonTypingUndoLevel(e);
          }

          return;
        }

        // If key isn't Ctrl+Alt/AltGr
        var modKey = (e.ctrlKey && !e.altKey) || e.metaKey;
        if ((keyCode < 16 || keyCode > 20) && keyCode !== 224 && keyCode !== 91 && !self.typing && !modKey) {
          self.beforeChange();
          setTyping(true);
          self.add({}, e);
          isFirstTypedCharacter = true;
        }
      });

      editor.on('MouseDown', function (e) {
        if (self.typing) {
          addNonTypingUndoLevel(e);
        }
      });

      // Add keyboard shortcuts for undo/redo keys
      editor.addShortcut('meta+z', '', 'Undo');
      editor.addShortcut('meta+y,meta+shift+z', '', 'Redo');

      editor.on('AddUndo Undo Redo ClearUndos', function (e) {
        if (!e.isDefaultPrevented()) {
          editor.nodeChanged();
        }
      });

      /*eslint consistent-this:0 */
      self = {
        // Explode for debugging reasons
        data: data,

        /**
         * State if the user is currently typing or not. This will add a typing operation into one undo
         * level instead of one new level for each keystroke.
         *
         * @field {Boolean} typing
         */
        typing: false,

        /**
         * Stores away a bookmark to be used when performing an undo action so that the selection is before
         * the change has been made.
         *
         * @method beforeChange
         */
        beforeChange: function () {
          if (isUnlocked()) {
            beforeBookmark = editor.selection.getBookmark(2, true);
          }
        },

        /**
         * Adds a new undo level/snapshot to the undo list.
         *
         * @method add
         * @param {Object} level Optional undo level object to add.
         * @param {DOMEvent} event Optional event responsible for the creation of the undo level.
         * @return {Object} Undo level that got added or null it a level wasn't needed.
         */
        add: function (level, event) {
          var i, settings = editor.settings, lastLevel, currentLevel;

          currentLevel = Levels.createFromEditor(editor);
          level = level || {};
          level = Tools.extend(level, currentLevel);

          if (isUnlocked() === false || editor.removed) {
            return null;
          }

          lastLevel = data[index];
          if (editor.fire('BeforeAddUndo', { level: level, lastLevel: lastLevel, originalEvent: event }).isDefaultPrevented()) {
            return null;
          }

          // Add undo level if needed
          if (lastLevel && Levels.isEq(lastLevel, level)) {
            return null;
          }

          // Set before bookmark on previous level
          if (data[index]) {
            data[index].beforeBookmark = beforeBookmark;
          }

          // Time to compress
          if (settings.custom_undo_redo_levels) {
            if (data.length > settings.custom_undo_redo_levels) {
              for (i = 0; i < data.length - 1; i++) {
                data[i] = data[i + 1];
              }

              data.length--;
              index = data.length;
            }
          }

          // Get a non intrusive normalized bookmark
          level.bookmark = editor.selection.getBookmark(2, true);

          // Crop array if needed
          if (index < data.length - 1) {
            data.length = index + 1;
          }

          data.push(level);
          index = data.length - 1;

          var args = { level: level, lastLevel: lastLevel, originalEvent: event };

          editor.fire('AddUndo', args);

          if (index > 0) {
            setDirty(true);
            editor.fire('change', args);
          }

          return level;
        },

        /**
         * Undoes the last action.
         *
         * @method undo
         * @return {Object} Undo level or null if no undo was performed.
         */
        undo: function () {
          var level;

          if (self.typing) {
            self.add();
            self.typing = false;
            setTyping(false);
          }

          if (index > 0) {
            level = data[--index];
            Levels.applyToEditor(editor, level, true);
            setDirty(true);
            editor.fire('undo', { level: level });
          }

          return level;
        },

        /**
         * Redoes the last action.
         *
         * @method redo
         * @return {Object} Redo level or null if no redo was performed.
         */
        redo: function () {
          var level;

          if (index < data.length - 1) {
            level = data[++index];
            Levels.applyToEditor(editor, level, false);
            setDirty(true);
            editor.fire('redo', { level: level });
          }

          return level;
        },

        /**
         * Removes all undo levels.
         *
         * @method clear
         */
        clear: function () {
          data = [];
          index = 0;
          self.typing = false;
          self.data = data;
          editor.fire('ClearUndos');
        },

        /**
         * Returns true/false if the undo manager has any undo levels.
         *
         * @method hasUndo
         * @return {Boolean} true/false if the undo manager has any undo levels.
         */
        hasUndo: function () {
          // Has undo levels or typing and content isn't the same as the initial level
          return index > 0 || (self.typing && data[0] && !Levels.isEq(Levels.createFromEditor(editor), data[0]));
        },

        /**
         * Returns true/false if the undo manager has any redo levels.
         *
         * @method hasRedo
         * @return {Boolean} true/false if the undo manager has any redo levels.
         */
        hasRedo: function () {
          return index < data.length - 1 && !self.typing;
        },

        /**
         * Executes the specified mutator function as an undo transaction. The selection
         * before the modification will be stored to the undo stack and if the DOM changes
         * it will add a new undo level. Any logic within the translation that adds undo levels will
         * be ignored. So a translation can include calls to execCommand or editor.insertContent.
         *
         * @method transact
         * @param {function} callback Function that gets executed and has dom manipulation logic in it.
         * @return {Object} Undo level that got added or null it a level wasn't needed.
         */
        transact: function (callback) {
          endTyping();
          self.beforeChange();
          self.ignore(callback);
          return self.add();
        },

        /**
         * Executes the specified mutator function as an undo transaction. But without adding an undo level.
         * Any logic within the translation that adds undo levels will be ignored. So a translation can
         * include calls to execCommand or editor.insertContent.
         *
         * @method ignore
         * @param {function} callback Function that gets executed and has dom manipulation logic in it.
         * @return {Object} Undo level that got added or null it a level wasn't needed.
         */
        ignore: function (callback) {
          try {
            locks++;
            callback();
          } finally {
            locks--;
          }
        },

        /**
         * Adds an extra "hidden" undo level by first applying the first mutation and store that to the undo stack
         * then roll back that change and do the second mutation on top of the stack. This will produce an extra
         * undo level that the user doesn't see until they undo.
         *
         * @method extra
         * @param {function} callback1 Function that does mutation but gets stored as a "hidden" extra undo level.
         * @param {function} callback2 Function that does mutation but gets displayed to the user.
         */
        extra: function (callback1, callback2) {
          var lastLevel, bookmark;

          if (self.transact(callback1)) {
            bookmark = data[index].bookmark;
            lastLevel = data[index - 1];
            Levels.applyToEditor(editor, lastLevel, true);

            if (self.transact(callback2)) {
              data[index - 1].beforeBookmark = bookmark;
            }
          }
        }
      };

      return self;
    };
  }
);

/**
 * NodePath.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 paths of nodes within an element.
 *
 * @private
 * @class tinymce.dom.NodePath
 */
define(
  'tinymce.core.dom.NodePath',
  [
    "tinymce.core.dom.DOMUtils"
  ],
  function (DOMUtils) {
    var create = function (rootNode, targetNode, normalized) {
      var path = [];

      for (; targetNode && targetNode != rootNode; targetNode = targetNode.parentNode) {
        path.push(DOMUtils.nodeIndex(targetNode, normalized));
      }

      return path;
    };

    var resolve = function (rootNode, path) {
      var i, node, children;

      for (node = rootNode, i = path.length - 1; i >= 0; i--) {
        children = node.childNodes;

        if (path[i] > children.length - 1) {
          return null;
        }

        node = children[path[i]];
      }

      return node;
    };

    return {
      create: create,
      resolve: resolve
    };
  }
);
/**
 * Quirks.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
 *
 * @ignore-file
 */

/**
 * This file includes fixes for various browser quirks it's made to make it easy to add/remove browser specific fixes.
 *
 * @private
 * @class tinymce.util.Quirks
 */
define(
  'tinymce.core.util.Quirks',
  [
    'global!document',
    'global!window',
    'tinymce.core.caret.CaretContainer',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.caret.CaretWalker',
    'tinymce.core.dom.NodePath',
    'tinymce.core.dom.RangeUtils',
    'tinymce.core.dom.TreeWalker',
    'tinymce.core.Env',
    'tinymce.core.html.Entities',
    'tinymce.core.html.Node',
    'tinymce.core.util.Delay',
    'tinymce.core.util.Tools',
    'tinymce.core.util.VK'
  ],
  function (document, window, CaretContainer, CaretPosition, CaretWalker, NodePath, RangeUtils, TreeWalker, Env, Entities, Node, Delay, Tools, VK) {
    return function (editor) {
      var each = Tools.each;
      var BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection,
        settings = editor.settings, parser = editor.parser;
      var isGecko = Env.gecko, isIE = Env.ie, isWebKit = Env.webkit;
      var mceInternalUrlPrefix = 'data:text/mce-internal,';
      var mceInternalDataType = isIE ? 'Text' : 'URL';

      /**
       * Executes a command with a specific state this can be to enable/disable browser editing features.
       */
      var setEditorCommandState = function (cmd, state) {
        try {
          editor.getDoc().execCommand(cmd, false, state);
        } catch (ex) {
          // Ignore
        }
      };

      /**
       * Returns true/false if the event is prevented or not.
       *
       * @private
       * @param {Event} e Event object.
       * @return {Boolean} true/false if the event is prevented or not.
       */
      var isDefaultPrevented = function (e) {
        return e.isDefaultPrevented();
      };

      /**
       * Sets Text/URL data on the event's dataTransfer object to a special data:text/mce-internal url.
       * This is to workaround the inability to set custom contentType on IE and Safari.
       * The editor's selected content is encoded into this url so drag and drop between editors will work.
       *
       * @private
       * @param {DragEvent} e Event object
       */
      var setMceInternalContent = function (e) {
        var selectionHtml, internalContent;

        if (e.dataTransfer) {
          if (editor.selection.isCollapsed() && e.target.tagName == 'IMG') {
            selection.select(e.target);
          }

          selectionHtml = editor.selection.getContent();

          // Safari/IE doesn't support custom dataTransfer items so we can only use URL and Text
          if (selectionHtml.length > 0) {
            internalContent = mceInternalUrlPrefix + escape(editor.id) + ',' + escape(selectionHtml);
            e.dataTransfer.setData(mceInternalDataType, internalContent);
          }
        }
      };

      /**
       * Gets content of special data:text/mce-internal url on the event's dataTransfer object.
       * This is to workaround the inability to set custom contentType on IE and Safari.
       * The editor's selected content is encoded into this url so drag and drop between editors will work.
       *
       * @private
       * @param {DragEvent} e Event object
       * @returns {String} mce-internal content
       */
      var getMceInternalContent = function (e) {
        var internalContent;

        if (e.dataTransfer) {
          internalContent = e.dataTransfer.getData(mceInternalDataType);

          if (internalContent && internalContent.indexOf(mceInternalUrlPrefix) >= 0) {
            internalContent = internalContent.substr(mceInternalUrlPrefix.length).split(',');

            return {
              id: unescape(internalContent[0]),
              html: unescape(internalContent[1])
            };
          }
        }

        return null;
      };

      /**
       * Inserts contents using the paste clipboard command if it's available if it isn't it will fallback
       * to the core command.
       *
       * @private
       * @param {String} content Content to insert at selection.
       * @param {Boolean} internal State if the paste is to be considered internal or external.
       */
      var insertClipboardContents = function (content, internal) {
        if (editor.queryCommandSupported('mceInsertClipboardContent')) {
          editor.execCommand('mceInsertClipboardContent', false, { content: content, internal: internal });
        } else {
          editor.execCommand('mceInsertContent', false, content);
        }
      };

      /**
       * Makes sure that the editor body becomes empty when backspace or delete is pressed in empty editors.
       *
       * For example:
       * <p><b>|</b></p>
       *
       * Or:
       * <h1>|</h1>
       *
       * Or:
       * [<h1></h1>]
       */
      var emptyEditorWhenDeleting = function () {
        var serializeRng = function (rng) {
          var body = dom.create("body");
          var contents = rng.cloneContents();
          body.appendChild(contents);
          return selection.serializer.serialize(body, { format: 'html' });
        };

        var allContentsSelected = function (rng) {
          var selection = serializeRng(rng);

          var allRng = dom.createRng();
          allRng.selectNode(editor.getBody());

          var allSelection = serializeRng(allRng);
          return selection === allSelection;
        };

        editor.on('keydown', function (e) {
          var keyCode = e.keyCode, isCollapsed, body;

          // Empty the editor if it's needed for example backspace at <p><b>|</b></p>
          if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) {
            isCollapsed = editor.selection.isCollapsed();
            body = editor.getBody();

            // Selection is collapsed but the editor isn't empty
            if (isCollapsed && !dom.isEmpty(body)) {
              return;
            }

            // Selection isn't collapsed but not all the contents is selected
            if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) {
              return;
            }

            // Manually empty the editor
            e.preventDefault();
            editor.setContent('');

            if (body.firstChild && dom.isBlock(body.firstChild)) {
              editor.selection.setCursorLocation(body.firstChild, 0);
            } else {
              editor.selection.setCursorLocation(body, 0);
            }

            editor.nodeChanged();
          }
        });
      };

      /**
       * WebKit doesn't select all the nodes in the body when you press Ctrl+A.
       * IE selects more than the contents <body>[<p>a</p>]</body> instead of <body><p>[a]</p]</body> see bug #6438
       * This selects the whole body so that backspace/delete logic will delete everything
       */
      var selectAll = function () {
        editor.shortcuts.add('meta+a', null, 'SelectAll');
      };

      /**
       * WebKit has a weird issue where it some times fails to properly convert keypresses to input method keystrokes.
       * The IME on Mac doesn't initialize when it doesn't fire a proper focus event.
       *
       * This seems to happen when the user manages to click the documentElement element then the window doesn't get proper focus until
       * you enter a character into the editor.
       *
       * It also happens when the first focus in made to the body.
       *
       * See: https://bugs.webkit.org/show_bug.cgi?id=83566
       */
      var inputMethodFocus = function () {
        if (!editor.settings.content_editable) {
          // Case 1 IME doesn't initialize if you focus the document
          // Disabled since it was interferring with the cE=false logic
          // Also coultn't reproduce the issue on Safari 9
          /*dom.bind(editor.getDoc(), 'focusin', function() {
            selection.setRng(selection.getRng());
          });*/

          // Case 2 IME doesn't initialize if you click the documentElement it also doesn't properly fire the focusin event
          // Needs to be both down/up due to weird rendering bug on Chrome Windows
          dom.bind(editor.getDoc(), 'mousedown mouseup', function (e) {
            var rng;

            if (e.target == editor.getDoc().documentElement) {
              rng = selection.getRng();
              editor.getBody().focus();

              if (e.type == 'mousedown') {
                if (CaretContainer.isCaretContainer(rng.startContainer)) {
                  return;
                }

                // Edge case for mousedown, drag select and mousedown again within selection on Chrome Windows to render caret
                selection.placeCaretAt(e.clientX, e.clientY);
              } else {
                selection.setRng(rng);
              }
            }
          });
        }
      };

      /**
       * Backspacing in FireFox/IE from a paragraph into a horizontal rule results in a floating text node because the
       * browser just deletes the paragraph - the browser fails to merge the text node with a horizontal rule so it is
       * left there. TinyMCE sees a floating text node and wraps it in a paragraph on the key up event (ForceBlocks.js
       * addRootBlocks), meaning the action does nothing. With this code, FireFox/IE matche the behaviour of other
       * browsers.
       *
       * It also fixes a bug on Firefox where it's impossible to delete HR elements.
       */
      var removeHrOnBackspace = function () {
        editor.on('keydown', function (e) {
          if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) {
            // Check if there is any HR elements this is faster since getRng on IE 7 & 8 is slow
            if (!editor.getBody().getElementsByTagName('hr').length) {
              return;
            }

            if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) {
              var node = selection.getNode();
              var previousSibling = node.previousSibling;

              if (node.nodeName == 'HR') {
                dom.remove(node);
                e.preventDefault();
                return;
              }

              if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") {
                dom.remove(previousSibling);
                e.preventDefault();
              }
            }
          }
        });
      };

      /**
       * Firefox 3.x has an issue where the body element won't get proper focus if you click out
       * side it's rectangle.
       */
      var focusBody = function () {
        // Fix for a focus bug in FF 3.x where the body element
        // wouldn't get proper focus if the user clicked on the HTML element
        if (!window.Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4
          editor.on('mousedown', function (e) {
            if (!isDefaultPrevented(e) && e.target.nodeName === "HTML") {
              var body = editor.getBody();

              // Blur the body it's focused but not correctly focused
              body.blur();

              // Refocus the body after a little while
              Delay.setEditorTimeout(editor, function () {
                body.focus();
              });
            }
          });
        }
      };

      /**
       * WebKit has a bug where it isn't possible to select image, hr or anchor elements
       * by clicking on them so we need to fake that.
       */
      var selectControlElements = function () {
        editor.on('click', function (e) {
          var target = e.target;

          // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250
          // WebKit can't even do simple things like selecting an image
          // Needs to be the setBaseAndExtend or it will fail to select floated images
          if (/^(IMG|HR)$/.test(target.nodeName) && dom.getContentEditableParent(target) !== "false") {
            e.preventDefault();
            editor.selection.select(target);
            editor.nodeChanged();
          }

          if (target.nodeName == 'A' && dom.hasClass(target, 'mce-item-anchor')) {
            e.preventDefault();
            selection.select(target);
          }
        });
      };

      /**
       * Fixes a Gecko bug where the style attribute gets added to the wrong element when deleting between two block elements.
       *
       * Fixes do backspace/delete on this:
       * <p>bla[ck</p><p style="color:red">r]ed</p>
       *
       * Would become:
       * <p>bla|ed</p>
       *
       * Instead of:
       * <p style="color:red">bla|ed</p>
       */
      var removeStylesWhenDeletingAcrossBlockElements = function () {
        var getAttributeApplyFunction = function () {
          var template = dom.getAttribs(selection.getStart().cloneNode(false));

          return function () {
            var target = selection.getStart();

            if (target !== editor.getBody()) {
              dom.setAttrib(target, "style", null);

              each(template, function (attr) {
                target.setAttributeNode(attr.cloneNode(true));
              });
            }
          };
        };

        var isSelectionAcrossElements = function () {
          return !selection.isCollapsed() &&
            dom.getParent(selection.getStart(), dom.isBlock) != dom.getParent(selection.getEnd(), dom.isBlock);
        };

        editor.on('keypress', function (e) {
          var applyAttributes;

          if (!isDefaultPrevented(e) && (e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) {
            applyAttributes = getAttributeApplyFunction();
            editor.getDoc().execCommand('delete', false, null);
            applyAttributes();
            e.preventDefault();
            return false;
          }
        });

        dom.bind(editor.getDoc(), 'cut', function (e) {
          var applyAttributes;

          if (!isDefaultPrevented(e) && isSelectionAcrossElements()) {
            applyAttributes = getAttributeApplyFunction();

            Delay.setEditorTimeout(editor, function () {
              applyAttributes();
            });
          }
        });
      };

      /**
       * Backspacing into a table behaves differently depending upon browser type.
       * Therefore, disable Backspace when cursor immediately follows a table.
       */
      var disableBackspaceIntoATable = function () {
        editor.on('keydown', function (e) {
          if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) {
            if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) {
              var previousSibling = selection.getNode().previousSibling;
              if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "table") {
                e.preventDefault();
                return false;
              }
            }
          }
        });
      };

      /**
       * Removes a blockquote when backspace is pressed at the beginning of it.
       *
       * For example:
       * <blockquote><p>|x</p></blockquote>
       *
       * Becomes:
       * <p>|x</p>
       */
      var removeBlockQuoteOnBackSpace = function () {
        // Add block quote deletion handler
        editor.on('keydown', function (e) {
          var rng, container, offset, root, parent;

          if (isDefaultPrevented(e) || e.keyCode != VK.BACKSPACE) {
            return;
          }

          rng = selection.getRng();
          container = rng.startContainer;
          offset = rng.startOffset;
          root = dom.getRoot();
          parent = container;

          if (!rng.collapsed || offset !== 0) {
            return;
          }

          while (parent && parent.parentNode && parent.parentNode.firstChild == parent && parent.parentNode != root) {
            parent = parent.parentNode;
          }

          // Is the cursor at the beginning of a blockquote?
          if (parent.tagName === 'BLOCKQUOTE') {
            // Remove the blockquote
            editor.formatter.toggle('blockquote', null, parent);

            // Move the caret to the beginning of container
            rng = dom.createRng();
            rng.setStart(container, 0);
            rng.setEnd(container, 0);
            selection.setRng(rng);
          }
        });
      };

      /**
       * Sets various Gecko editing options on mouse down and before a execCommand to disable inline table editing that is broken etc.
       */
      var setGeckoEditingOptions = function () {
        var setOpts = function () {
          refreshContentEditable();

          setEditorCommandState("StyleWithCSS", false);
          setEditorCommandState("enableInlineTableEditing", false);

          if (!settings.object_resizing) {
            setEditorCommandState("enableObjectResizing", false);
          }
        };

        if (!settings.readonly) {
          editor.on('BeforeExecCommand MouseDown', setOpts);
        }
      };

      /**
       * Fixes a gecko link bug, when a link is placed at the end of block elements there is
       * no way to move the caret behind the link. This fix adds a bogus br element after the link.
       *
       * For example this:
       * <p><b><a href="#">x</a></b></p>
       *
       * Becomes this:
       * <p><b><a href="#">x</a></b><br></p>
       */
      var addBrAfterLastLinks = function () {
        var fixLinks = function () {
          each(dom.select('a'), function (node) {
            var parentNode = node.parentNode, root = dom.getRoot();

            if (parentNode.lastChild === node) {
              while (parentNode && !dom.isBlock(parentNode)) {
                if (parentNode.parentNode.lastChild !== parentNode || parentNode === root) {
                  return;
                }

                parentNode = parentNode.parentNode;
              }

              dom.add(parentNode, 'br', { 'data-mce-bogus': 1 });
            }
          });
        };

        editor.on('SetContent ExecCommand', function (e) {
          if (e.type == "setcontent" || e.command === 'mceInsertLink') {
            fixLinks();
          }
        });
      };

      /**
       * WebKit will produce DIV elements here and there by default. But since TinyMCE uses paragraphs by
       * default we want to change that behavior.
       */
      var setDefaultBlockType = function () {
        if (settings.forced_root_block) {
          editor.on('init', function () {
            setEditorCommandState('DefaultParagraphSeparator', settings.forced_root_block);
          });
        }
      };

      /**
       * Fixes selection issues where the caret can be placed between two inline elements like <b>a</b>|<b>b</b>
       * this fix will lean the caret right into the closest inline element.
       */
      var normalizeSelection = function () {
        // Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i> except for Ctrl+A since it selects everything
        editor.on('keyup focusin mouseup', function (e) {
          if (e.keyCode != 65 || !VK.metaKeyPressed(e)) {
            // We can't normalize on non collapsed ranges on keyboard events since that would cause
            // issues with moving the selection over empty paragraphs. See #TINY-1130
            if (e.type !== 'keyup' || editor.selection.isCollapsed()) {
              selection.normalize();
            }
          }
        }, true);
      };

      /**
       * Forces Gecko to render a broken image icon if it fails to load an image.
       */
      var showBrokenImageIcon = function () {
        editor.contentStyles.push(
          'img:-moz-broken {' +
          '-moz-force-broken-image-icon:1;' +
          'min-width:24px;' +
          'min-height:24px' +
          '}'
        );
      };

      /**
       * iOS has a bug where it's impossible to type if the document has a touchstart event
       * bound and the user touches the document while having the on screen keyboard visible.
       *
       * The touch event moves the focus to the parent document while having the caret inside the iframe
       * this fix moves the focus back into the iframe document.
       */
      var restoreFocusOnKeyDown = function () {
        if (!editor.inline) {
          editor.on('keydown', function () {
            if (document.activeElement == document.body) {
              editor.getWin().focus();
            }
          });
        }
      };

      /**
       * IE 11 has an annoying issue where you can't move focus into the editor
       * by clicking on the white area HTML element. We used to be able to to fix this with
       * the fixCaretSelectionOfDocumentElementOnIe fix. But since M$ removed the selection
       * object it's not possible anymore. So we need to hack in a ungly CSS to force the
       * body to be at least 150px. If the user clicks the HTML element out side this 150px region
       * we simply move the focus into the first paragraph. Not ideal since you loose the
       * positioning of the caret but goot enough for most cases.
       */
      var bodyHeight = function () {
        if (!editor.inline) {
          editor.contentStyles.push('body {min-height: 150px}');
          editor.on('click', function (e) {
            var rng;

            if (e.target.nodeName == 'HTML') {
              // Edge seems to only need focus if we set the range
              // the caret will become invisible and moved out of the iframe!!
              if (Env.ie > 11) {
                editor.getBody().focus();
                return;
              }

              // Need to store away non collapsed ranges since the focus call will mess that up see #7382
              rng = editor.selection.getRng();
              editor.getBody().focus();
              editor.selection.setRng(rng);
              editor.selection.normalize();
              editor.nodeChanged();
            }
          });
        }
      };

      /**
       * Firefox on Mac OS will move the browser back to the previous page if you press CMD+Left arrow.
       * You might then loose all your work so we need to block that behavior and replace it with our own.
       */
      var blockCmdArrowNavigation = function () {
        if (Env.mac) {
          editor.on('keydown', function (e) {
            if (VK.metaKeyPressed(e) && !e.shiftKey && (e.keyCode == 37 || e.keyCode == 39)) {
              e.preventDefault();
              editor.selection.getSel().modify('move', e.keyCode == 37 ? 'backward' : 'forward', 'lineboundary');
            }
          });
        }
      };

      /**
       * Disables the autolinking in IE 9+ this is then re-enabled by the autolink plugin.
       */
      var disableAutoUrlDetect = function () {
        setEditorCommandState("AutoUrlDetect", false);
      };

      /**
       * iOS 7.1 introduced two new bugs:
       * 1) It's possible to open links within a contentEditable area by clicking on them.
       * 2) If you hold down the finger it will display the link/image touch callout menu.
       */
      var tapLinksAndImages = function () {
        editor.on('click', function (e) {
          var elm = e.target;

          do {
            if (elm.tagName === 'A') {
              e.preventDefault();
              return;
            }
          } while ((elm = elm.parentNode));
        });

        editor.contentStyles.push('.mce-content-body {-webkit-touch-callout: none}');
      };

      /**
       * iOS Safari and possible other browsers have a bug where it won't fire
       * a click event when a contentEditable is focused. This function fakes click events
       * by using touchstart/touchend and measuring the time and distance travelled.
       */
      /*
      function touchClickEvent() {
        editor.on('touchstart', function(e) {
          var elm, time, startTouch, changedTouches;

          elm = e.target;
          time = new Date().getTime();
          changedTouches = e.changedTouches;

          if (!changedTouches || changedTouches.length > 1) {
            return;
          }

          startTouch = changedTouches[0];

          editor.once('touchend', function(e) {
            var endTouch = e.changedTouches[0], args;

            if (new Date().getTime() - time > 500) {
              return;
            }

            if (Math.abs(startTouch.clientX - endTouch.clientX) > 5) {
              return;
            }

            if (Math.abs(startTouch.clientY - endTouch.clientY) > 5) {
              return;
            }

            args = {
              target: elm
            };

            each('pageX pageY clientX clientY screenX screenY'.split(' '), function(key) {
              args[key] = endTouch[key];
            });

            args = editor.fire('click', args);

            if (!args.isDefaultPrevented()) {
              // iOS WebKit can't place the caret properly once
              // you bind touch events so we need to do this manually
              // TODO: Expand to the closest word? Touble tap still works.
              editor.selection.placeCaretAt(endTouch.clientX, endTouch.clientY);
              editor.nodeChanged();
            }
          });
        });
      }
      */

      /**
       * WebKit has a bug where it will allow forms to be submitted if they are inside a contentEditable element.
       * For example this: <form><button></form>
       */
      var blockFormSubmitInsideEditor = function () {
        editor.on('init', function () {
          editor.dom.bind(editor.getBody(), 'submit', function (e) {
            e.preventDefault();
          });
        });
      };

      /**
       * Sometimes WebKit/Blink generates BR elements with the Apple-interchange-newline class.
       *
       * Scenario:
       *  1) Create a table 2x2.
       *  2) Select and copy cells A2-B2.
       *  3) Paste and it will add BR element to table cell.
       */
      var removeAppleInterchangeBrs = function () {
        parser.addNodeFilter('br', function (nodes) {
          var i = nodes.length;

          while (i--) {
            if (nodes[i].attr('class') == 'Apple-interchange-newline') {
              nodes[i].remove();
            }
          }
        });
      };

      /**
       * IE cannot set custom contentType's on drag events, and also does not properly drag/drop between
       * editors. This uses a special data:text/mce-internal URL to pass data when drag/drop between editors.
       */
      var ieInternalDragAndDrop = function () {
        editor.on('dragstart', function (e) {
          setMceInternalContent(e);
        });

        editor.on('drop', function (e) {
          if (!isDefaultPrevented(e)) {
            var internalContent = getMceInternalContent(e);

            if (internalContent && internalContent.id != editor.id) {
              e.preventDefault();

              var rng = RangeUtils.getCaretRangeFromPoint(e.x, e.y, editor.getDoc());
              selection.setRng(rng);
              insertClipboardContents(internalContent.html, true);
            }
          }
        });
      };

      var refreshContentEditable = function () {
        // No-op since Mozilla seems to have fixed the caret repaint issues
      };

      var isHidden = function () {
        var sel;

        if (!isGecko || editor.removed) {
          return 0;
        }

        // Weird, wheres that cursor selection?
        sel = editor.selection.getSel();
        return (!sel || !sel.rangeCount || sel.rangeCount === 0);
      };

      // All browsers
      removeBlockQuoteOnBackSpace();
      emptyEditorWhenDeleting();

      // Windows phone will return a range like [body, 0] on mousedown so
      // it will always normalize to the wrong location
      if (!Env.windowsPhone) {
        normalizeSelection();
      }

      // WebKit
      if (isWebKit) {
        inputMethodFocus();
        selectControlElements();
        setDefaultBlockType();
        blockFormSubmitInsideEditor();
        disableBackspaceIntoATable();
        removeAppleInterchangeBrs();

        //touchClickEvent();

        // iOS
        if (Env.iOS) {
          restoreFocusOnKeyDown();
          bodyHeight();
          tapLinksAndImages();
        } else {
          selectAll();
        }
      }

      if (Env.ie >= 11) {
        bodyHeight();
        disableBackspaceIntoATable();
      }

      if (Env.ie) {
        selectAll();
        disableAutoUrlDetect();
        ieInternalDragAndDrop();
      }

      // Gecko
      if (isGecko) {
        removeHrOnBackspace();
        focusBody();
        removeStylesWhenDeletingAcrossBlockElements();
        setGeckoEditingOptions();
        addBrAfterLastLinks();
        showBrokenImageIcon();
        blockCmdArrowNavigation();
        disableBackspaceIntoATable();
      }

      return {
        refreshContentEditable: refreshContentEditable,
        isHidden: isHidden
      };
    };
  }
);

/**
 * InitContentBody.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
 */

define(
  'tinymce.core.init.InitContentBody',
  [
    'ephox.sugar.api.dom.Insert',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.properties.Attr',
    'global!document',
    'global!window',
    'tinymce.core.api.Formatter',
    'tinymce.core.caret.CaretContainerInput',
    'tinymce.core.dom.DOMUtils',
    'tinymce.core.dom.Selection',
    'tinymce.core.dom.Serializer',
    'tinymce.core.EditorUpload',
    'tinymce.core.ErrorReporter',
    'tinymce.core.ForceBlocks',
    'tinymce.core.html.DomParser',
    'tinymce.core.html.Node',
    'tinymce.core.html.Schema',
    'tinymce.core.keyboard.KeyboardOverrides',
    'tinymce.core.NodeChange',
    'tinymce.core.SelectionOverrides',
    'tinymce.core.UndoManager',
    'tinymce.core.util.Delay',
    'tinymce.core.util.Quirks',
    'tinymce.core.util.Tools'
  ],
  function (
    Insert, Element, Attr, document, window, Formatter, CaretContainerInput, DOMUtils, Selection, Serializer, EditorUpload, ErrorReporter, ForceBlocks, DomParser,
    Node, Schema, KeyboardOverrides, NodeChange, SelectionOverrides, UndoManager, Delay, Quirks, Tools
  ) {
    var DOM = DOMUtils.DOM;

    var appendStyle = function (editor, text) {
      var head = Element.fromDom(editor.getDoc().head);
      var tag = Element.fromTag('style');
      Attr.set(tag, 'type', 'text/css');
      Insert.append(tag, Element.fromText(text));
      Insert.append(head, tag);
    };

    var createParser = function (editor) {
      var parser = new DomParser(editor.settings, editor.schema);

      // Convert src and href into data-mce-src, data-mce-href and data-mce-style
      parser.addAttributeFilter('src,href,style,tabindex', function (nodes, name) {
        var i = nodes.length, node, dom = editor.dom, value, internalName;

        while (i--) {
          node = nodes[i];
          value = node.attr(name);
          internalName = 'data-mce-' + name;

          // Add internal attribute if we need to we don't on a refresh of the document
          if (!node.attributes.map[internalName]) {
            // Don't duplicate these since they won't get modified by any browser
            if (value.indexOf('data:') === 0 || value.indexOf('blob:') === 0) {
              continue;
            }

            if (name === "style") {
              value = dom.serializeStyle(dom.parseStyle(value), node.name);

              if (!value.length) {
                value = null;
              }

              node.attr(internalName, value);
              node.attr(name, value);
            } else if (name === "tabindex") {
              node.attr(internalName, value);
              node.attr(name, null);
            } else {
                if(!editor.settings.images_no_converturl || name != 'src' && node.name != 'img')
                    node.attr(internalName, editor.convertURL(value, name, node.name));
            }
          }
        }
      });

      // Keep scripts from executing
      parser.addNodeFilter('script', function (nodes) {
        var i = nodes.length, node, type;

        while (i--) {
          node = nodes[i];
          type = node.attr('type') || 'no/type';
          if (type.indexOf('mce-') !== 0) {
            node.attr('type', 'mce-' + type);
          }
        }
      });

      parser.addNodeFilter('#cdata', function (nodes) {
        var i = nodes.length, node;

        while (i--) {
          node = nodes[i];
          node.type = 8;
          node.name = '#comment';
          node.value = '[CDATA[' + node.value + ']]';
        }
      });

      parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function (nodes) {
        var i = nodes.length, node, nonEmptyElements = editor.schema.getNonEmptyElements();

        while (i--) {
          node = nodes[i];

          if (node.isEmpty(nonEmptyElements) && node.getAll('br').length === 0) {
            node.append(new Node('br', 1)).shortEnded = true;
          }
        }
      });

      return parser;
    };

    var autoFocus = function (editor) {
      if (editor.settings.auto_focus) {
        Delay.setEditorTimeout(editor, function () {
          var focusEditor;

          if (editor.settings.auto_focus === true) {
            focusEditor = editor;
          } else {
            focusEditor = editor.editorManager.get(editor.settings.auto_focus);
          }

          if (!focusEditor.destroyed) {
            focusEditor.focus();
          }
        }, 100);
      }
    };

    var initEditor = function (editor) {
      editor.bindPendingEventDelegates();
      editor.initialized = true;
      editor.fire('init');
      editor.focus(true);
      editor.nodeChanged({ initial: true });
      editor.execCallback('init_instance_callback', editor);
      autoFocus(editor);
    };

    var getStyleSheetLoader = function (editor) {
      return editor.inline ? DOM.styleSheetLoader : editor.dom.styleSheetLoader;
    };

    var initContentBody = function (editor, skipWrite) {
      var settings = editor.settings, targetElm = editor.getElement(), doc = editor.getDoc(), body, contentCssText;

      // Restore visibility on target element
      if (!settings.inline) {
        editor.getElement().style.visibility = editor.orgVisibility;
      }

      // Setup iframe body
      if (!skipWrite && !settings.content_editable) {
        doc.open();
        doc.write(editor.iframeHTML);
        doc.close();
      }

      if (settings.content_editable) {
        editor.on('remove', function () {
          var bodyEl = this.getBody();

          DOM.removeClass(bodyEl, 'mce-content-body');
          DOM.removeClass(bodyEl, 'mce-edit-focus');
          DOM.setAttrib(bodyEl, 'contentEditable', null);
        });

        DOM.addClass(targetElm, 'mce-content-body');
        editor.contentDocument = doc = settings.content_document || document;
        editor.contentWindow = settings.content_window || window;
        editor.bodyElement = targetElm;

        // Prevent leak in IE
        settings.content_document = settings.content_window = null;

        // TODO: Fix this
        settings.root_name = targetElm.nodeName.toLowerCase();
      }

      // It will not steal focus while setting contentEditable
      body = editor.getBody();
      body.disabled = true;
      editor.readonly = settings.readonly;

      if (!editor.readonly) {
        if (editor.inline && DOM.getStyle(body, 'position', true) === 'static') {
          body.style.position = 'relative';
        }

        body.contentEditable = editor.getParam('content_editable_state', true);
      }

      body.disabled = false;

      editor.editorUpload = new EditorUpload(editor);
      editor.schema = new Schema(settings);
      editor.dom = new DOMUtils(doc, {
        keep_values: true,
        url_converter: editor.convertURL,
        url_converter_scope: editor,
        hex_colors: settings.force_hex_style_colors,
        class_filter: settings.class_filter,
        update_styles: true,
        root_element: editor.inline ? editor.getBody() : null,
        collect: settings.content_editable,
        schema: editor.schema,
        onSetAttrib: function (e) {
          editor.fire('SetAttrib', e);
        }
      });

      editor.parser = createParser(editor);
      editor.serializer = new Serializer(settings, editor);
      editor.selection = new Selection(editor.dom, editor.getWin(), editor.serializer, editor);
      editor.formatter = new Formatter(editor);
      editor.undoManager = new UndoManager(editor);
      editor._nodeChangeDispatcher = new NodeChange(editor);
      editor._selectionOverrides = new SelectionOverrides(editor);

      CaretContainerInput.setup(editor);
      KeyboardOverrides.setup(editor);
      ForceBlocks.setup(editor);

      editor.fire('PreInit');

      if (!settings.browser_spellcheck && !settings.gecko_spellcheck) {
        doc.body.spellcheck = false; // Gecko
        DOM.setAttrib(body, "spellcheck", "false");
      }

      editor.quirks = new Quirks(editor);
      editor.fire('PostRender');

      if (settings.directionality) {
        body.dir = settings.directionality;
      }

      if (settings.nowrap) {
        body.style.whiteSpace = "nowrap";
      }

      if (settings.protect) {
        editor.on('BeforeSetContent', function (e) {
          Tools.each(settings.protect, function (pattern) {
            e.content = e.content.replace(pattern, function (str) {
              return '<!--mce:protected ' + escape(str) + '-->';
            });
          });
        });
      }

      editor.on('SetContent', function () {
        editor.addVisual(editor.getBody());
      });

      // Remove empty contents
      if (settings.padd_empty_editor) {
        editor.on('PostProcess', function (e) {
          e.content = e.content.replace(/^(<p[^>]*>(&nbsp;|&#160;|\s|\u00a0|<br \/>|)<\/p>[\r\n]*|<br \/>[\r\n]*)$/, '');
        });
      }

      editor.load({ initial: true, format: 'html' });
      editor.startContent = editor.getContent({ format: 'raw' });

      editor.on('compositionstart compositionend', function (e) {
        editor.composing = e.type === 'compositionstart';
      });

      // Add editor specific CSS styles
      if (editor.contentStyles.length > 0) {
        contentCssText = '';

        Tools.each(editor.contentStyles, function (style) {
          contentCssText += style + "\r\n";
        });

        editor.dom.addStyle(contentCssText);
      }

      getStyleSheetLoader(editor).loadAll(
        editor.contentCSS,
        function (_) {
          initEditor(editor);
        },
        function (urls) {
          initEditor(editor);
        }
      );

      // Append specified content CSS last
      if (settings.content_style) {
        appendStyle(editor, settings.content_style);
      }
    };

    return {
      initContentBody: initContentBody
    };
  }
);

/**
 * PluginManager.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
 */

define(
  'tinymce.core.PluginManager',
  [
    'tinymce.core.AddOnManager'
  ],
  function (AddOnManager) {
    return AddOnManager.PluginManager;
  }
);

/**
 * ThemeManager.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
 */

define(
  'tinymce.core.ThemeManager',
  [
    'tinymce.core.AddOnManager'
  ],
  function (AddOnManager) {
    return AddOnManager.ThemeManager;
  }
);

/**
 * Init.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
 */

define(
  'tinymce.core.init.Init',
  [
    'ephox.katamari.api.Type',
    'global!document',
    'global!window',
    'tinymce.core.dom.DOMUtils',
    'tinymce.core.Env',
    'tinymce.core.init.InitContentBody',
    'tinymce.core.PluginManager',
    'tinymce.core.ThemeManager',
    'tinymce.core.util.Tools',
    'tinymce.core.util.Uuid'
  ],
  function (Type, document, window, DOMUtils, Env, InitContentBody, PluginManager, ThemeManager, Tools, Uuid) {
    var DOM = DOMUtils.DOM;

    var initPlugin = function (editor, initializedPlugins, plugin) {
      var Plugin = PluginManager.get(plugin), pluginUrl, pluginInstance;

      pluginUrl = PluginManager.urls[plugin] || editor.documentBaseUrl.replace(/\/$/, '');
      plugin = Tools.trim(plugin);
      if (Plugin && Tools.inArray(initializedPlugins, plugin) === -1) {
        Tools.each(PluginManager.dependencies(plugin), function (dep) {
          initPlugin(editor, initializedPlugins, dep);
        });

        if (editor.plugins[plugin]) {
          return;
        }

        pluginInstance = new Plugin(editor, pluginUrl, editor.$);

        editor.plugins[plugin] = pluginInstance;

        if (pluginInstance.init) {
          pluginInstance.init(editor, pluginUrl);
          initializedPlugins.push(plugin);
        }
      }
    };

    var trimLegacyPrefix = function (name) {
      // Themes and plugins can be prefixed with - to prevent them from being lazy loaded
      return name.replace(/^\-/, '');
    };

    var initPlugins = function (editor) {
      var initializedPlugins = [];

      Tools.each(editor.settings.plugins.split(/[ ,]/), function (name) {
        initPlugin(editor, initializedPlugins, trimLegacyPrefix(name));
      });
    };

    var initTheme = function (editor) {
      var Theme, theme = editor.settings.theme;

      if (Type.isString(theme)) {
        editor.settings.theme = trimLegacyPrefix(theme);

        Theme = ThemeManager.get(theme);
        editor.theme = new Theme(editor, ThemeManager.urls[theme]);

        if (editor.theme.init) {
          editor.theme.init(editor, ThemeManager.urls[theme] || editor.documentBaseUrl.replace(/\/$/, ''), editor.$);
        }
      } else {
        // Theme set to false or null doesn't produce a theme api
        editor.theme = {};
      }
    };

    var renderFromLoadedTheme = function (editor) {
      var w, h, minHeight, re, info, settings = editor.settings, elm = editor.getElement();

      w = settings.width || DOM.getStyle(elm, 'width') || '100%';
      h = settings.height || DOM.getStyle(elm, 'height') || elm.offsetHeight;
      minHeight = settings.min_height || 100;
      re = /^[0-9\.]+(|px)$/i;

      if (re.test('' + w)) {
        w = Math.max(parseInt(w, 10), 100);
      }

      if (re.test('' + h)) {
        h = Math.max(parseInt(h, 10), minHeight);
      }

      // Render UI
      info = editor.theme.renderUI({
        targetNode: elm,
        width: w,
        height: h,
        deltaWidth: settings.delta_width,
        deltaHeight: settings.delta_height
      });

      // Resize editor
      if (!settings.content_editable) {
        h = (info.iframeHeight || h) + (typeof h === 'number' ? (info.deltaHeight || 0) : '');
        if (h < minHeight) {
          h = minHeight;
        }
      }

      info.height = h;

      return info;
    };

    var renderFromThemeFunc = function (editor) {
      var info, elm = editor.getElement();

      info = editor.settings.theme(editor, elm);

      if (info.editorContainer.nodeType) {
        info.editorContainer.id = info.editorContainer.id || editor.id + "_parent";
      }

      if (info.iframeContainer && info.iframeContainer.nodeType) {
        info.iframeContainer.id = info.iframeContainer.id || editor.id + "_iframecontainer";
      }

      info.height = info.iframeHeight ? info.iframeHeight : elm.offsetHeight;

      return info;
    };

    var createThemeFalseResult = function (element) {
      return {
        editorContainer: element,
        iframeContainer: element
      };
    };

    var renderThemeFalseIframe = function (targetElement) {
      var iframeContainer = DOM.create('div');

      DOM.insertAfter(iframeContainer, targetElement);

      return createThemeFalseResult(iframeContainer);
    };

    var renderThemeFalse = function (editor) {
      var targetElement = editor.getElement();
      return editor.inline ? createThemeFalseResult(targetElement) : renderThemeFalseIframe(targetElement);
    };

    var renderThemeUi = function (editor) {
      var settings = editor.settings, elm = editor.getElement();

      editor.orgDisplay = elm.style.display;

      if (Type.isString(settings.theme)) {
        return renderFromLoadedTheme(editor);
      } else if (Type.isFunction(settings.theme)) {
        return renderFromThemeFunc(editor);
      } else {
        return renderThemeFalse(editor);
      }
    };

    var relaxDomain = function (editor, ifr) {
      // Domain relaxing is required since the user has messed around with document.domain
      // This only applies to IE 11 other browsers including Edge seems to handle document.domain
      if (document.domain !== window.location.hostname && Env.ie && Env.ie < 12) {
        var bodyUuid = Uuid.uuid('mce');

        editor[bodyUuid] = function () {
          InitContentBody.initContentBody(editor);
        };

        /*eslint no-script-url:0 */
        var domainRelaxUrl = 'javascript:(function(){' +
          'document.open();document.domain="' + document.domain + '";' +
          'var ed = window.parent.tinymce.get("' + editor.id + '");document.write(ed.iframeHTML);' +
          'document.close();ed.' + bodyUuid + '(true);})()';

        DOM.setAttrib(ifr, 'src', domainRelaxUrl);
        return true;
      }

      return false;
    };

    var createIframe = function (editor, o) {
      var settings = editor.settings, bodyId, bodyClass;

      editor.iframeHTML = settings.doctype + '<html><head>';

      // We only need to override paths if we have to
      // IE has a bug where it remove site absolute urls to relative ones if this is specified
      if (settings.document_base_url != editor.documentBaseUrl) {
        editor.iframeHTML += '<base href="' + editor.documentBaseURI.getURI() + '" />';
      }

      // IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode.
      if (!Env.caretAfter && settings.ie7_compat) {
        editor.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=7" />';
      }

      editor.iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />';

      bodyId = settings.body_id || 'tinymce';
      if (bodyId.indexOf('=') != -1) {
        bodyId = editor.getParam('body_id', '', 'hash');
        bodyId = bodyId[editor.id] || bodyId;
      }

      bodyClass = settings.body_class || '';
      if (bodyClass.indexOf('=') != -1) {
        bodyClass = editor.getParam('body_class', '', 'hash');
        bodyClass = bodyClass[editor.id] || '';
      }

      if (settings.content_security_policy) {
        editor.iframeHTML += '<meta http-equiv="Content-Security-Policy" content="' + settings.content_security_policy + '" />';
      }

      editor.iframeHTML += '</head><body id="' + bodyId +
        '" class="mce-content-body ' + bodyClass +
        '" data-id="' + editor.id + '"><br></body></html>';

      // Create iframe
      // TODO: ACC add the appropriate description on this.
      var ifr = DOM.create('iframe', {
        id: editor.id + "_ifr",
        frameBorder: '0',
        allowTransparency: "true",
        title: editor.editorManager.translate(
          "Rich Text Area. Press ALT-F9 for menu. " +
          "Press ALT-F10 for toolbar. Press ALT-0 for help"
        ),
        style: {
          width: '100%',
          height: o.height,
          display: 'block' // Important for Gecko to render the iframe correctly
        }
      });

      ifr.onload = function () {
        ifr.onload = null;
        editor.fire("load");
      };

      var isDomainRelaxed = relaxDomain(editor, ifr);

      editor.contentAreaContainer = o.iframeContainer;
      editor.iframeElement = ifr;

      DOM.add(o.iframeContainer, ifr);

      return isDomainRelaxed;
    };

    var init = function (editor) {
      var settings = editor.settings, elm = editor.getElement(), boxInfo;

      editor.rtl = settings.rtl_ui || editor.editorManager.i18n.rtl;
      editor.editorManager.i18n.setCode(settings.language);
      settings.aria_label = settings.aria_label || DOM.getAttrib(elm, 'aria-label', editor.getLang('aria.rich_text_area'));

      editor.fire('ScriptsLoaded');

      initTheme(editor);
      initPlugins(editor);
      boxInfo = renderThemeUi(editor);
      editor.editorContainer = boxInfo.editorContainer;

      // Load specified content CSS last
      if (settings.content_css) {
        Tools.each(Tools.explode(settings.content_css), function (u) {
          editor.contentCSS.push(editor.documentBaseURI.toAbsolute(u));
        });
      }

      // Content editable mode ends here
      if (settings.content_editable) {
        return InitContentBody.initContentBody(editor);
      }

      var isDomainRelaxed = createIframe(editor, boxInfo);

      if (boxInfo.editorContainer) {
        DOM.get(boxInfo.editorContainer).style.display = editor.orgDisplay;
        editor.hidden = DOM.isHidden(boxInfo.editorContainer);
      }

      editor.getElement().style.display = 'none';
      DOM.setAttrib(editor.id, 'aria-hidden', true);

      if (!isDomainRelaxed) {
        InitContentBody.initContentBody(editor);
      }
    };

    return {
      init: init
    };
  }
);

/**
 * Render.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
 */

define(
  'tinymce.core.init.Render',
  [
    'ephox.katamari.api.Type',
    'global!window',
    'tinymce.core.api.NotificationManager',
    'tinymce.core.api.WindowManager',
    'tinymce.core.dom.DOMUtils',
    'tinymce.core.dom.EventUtils',
    'tinymce.core.dom.ScriptLoader',
    'tinymce.core.Env',
    'tinymce.core.ErrorReporter',
    'tinymce.core.init.Init',
    'tinymce.core.PluginManager',
    'tinymce.core.ThemeManager',
    'tinymce.core.util.Tools'
  ],
  function (Type, window, NotificationManager, WindowManager, DOMUtils, EventUtils, ScriptLoader, Env, ErrorReporter, Init, PluginManager, ThemeManager, Tools) {
    var DOM = DOMUtils.DOM;

    var hasSkipLoadPrefix = function (name) {
      return name.charAt(0) === '-';
    };

    var loadLanguage = function (scriptLoader, editor) {
      var settings = editor.settings;

      if (settings.language && settings.language !== 'en' && !settings.language_url) {
        settings.language_url = editor.editorManager.baseURL + '/langs/' + settings.language + '.js';
      }

      if (settings.language_url) {
        scriptLoader.add(settings.language_url);
      }
    };

    var loadTheme = function (scriptLoader, editor, suffix, callback) {
      var settings = editor.settings, theme = settings.theme;

      if (Type.isString(theme)) {
        if (!hasSkipLoadPrefix(theme) && !ThemeManager.urls.hasOwnProperty(theme)) {
          var themeUrl = settings.theme_url;

          if (themeUrl) {
            ThemeManager.load(theme, editor.documentBaseURI.toAbsolute(themeUrl));
          } else {
            ThemeManager.load(theme, 'themes/' + theme + '/theme' + suffix + '.js');
          }
        }

        scriptLoader.loadQueue(function () {
          ThemeManager.waitFor(theme, callback);
        });
      } else {
        callback();
      }
    };

    var loadPlugins = function (settings, suffix) {
      if (Tools.isArray(settings.plugins)) {
        settings.plugins = settings.plugins.join(' ');
      }

      Tools.each(settings.external_plugins, function (url, name) {
        PluginManager.load(name, url);
        settings.plugins += ' ' + name;
      });

      Tools.each(settings.plugins.split(/[ ,]/), function (plugin) {
        plugin = Tools.trim(plugin);

        if (plugin && !PluginManager.urls[plugin]) {
          if (hasSkipLoadPrefix(plugin)) {
            plugin = plugin.substr(1, plugin.length);

            var dependencies = PluginManager.dependencies(plugin);

            Tools.each(dependencies, function (dep) {
              var defaultSettings = {
                prefix: 'plugins/',
                resource: dep,
                suffix: '/plugin' + suffix + '.js'
              };

              dep = PluginManager.createUrl(defaultSettings, dep);
              PluginManager.load(dep.resource, dep);
            });
          } else {
            PluginManager.load(plugin, {
              prefix: 'plugins/',
              resource: plugin,
              suffix: '/plugin' + suffix + '.js'
            });
          }
        }
      });
    };

    var loadScripts = function (editor, suffix) {
      var scriptLoader = ScriptLoader.ScriptLoader;

      loadTheme(scriptLoader, editor, suffix, function () {
        loadLanguage(scriptLoader, editor);
        loadPlugins(editor.settings, suffix);

        scriptLoader.loadQueue(function () {
          if (!editor.removed) {
            Init.init(editor);
          }
        }, editor, function (urls) {
          ErrorReporter.pluginLoadError(editor, urls[0]);

          if (!editor.removed) {
            Init.init(editor);
          }
        });
      });
    };

    var render = function (editor) {
      var settings = editor.settings, id = editor.id;

      var readyHandler = function () {
        DOM.unbind(window, 'ready', readyHandler);
        editor.render();
      };

      // Page is not loaded yet, wait for it
      if (!EventUtils.Event.domLoaded) {
        DOM.bind(window, 'ready', readyHandler);
        return;
      }

      // Element not found, then skip initialization
      if (!editor.getElement()) {
        return;
      }

      // No editable support old iOS versions etc
      if (!Env.contentEditable) {
        return;
      }

      // Hide target element early to prevent content flashing
      if (!settings.inline) {
        editor.orgVisibility = editor.getElement().style.visibility;
        editor.getElement().style.visibility = 'hidden';
      } else {
        editor.inline = true;
      }

      var form = editor.getElement().form || DOM.getParent(id, 'form');
      if (form) {
        editor.formElement = form;

        // Add hidden input for non input elements inside form elements
        if (settings.hidden_input && !/TEXTAREA|INPUT/i.test(editor.getElement().nodeName)) {
          DOM.insertAfter(DOM.create('input', { type: 'hidden', name: id }), id);
          editor.hasHiddenInput = true;
        }

        // Pass submit/reset from form to editor instance
        editor.formEventDelegate = function (e) {
          editor.fire(e.type, e);
        };

        DOM.bind(form, 'submit reset', editor.formEventDelegate);

        // Reset contents in editor when the form is reset
        editor.on('reset', function () {
          editor.setContent(editor.startContent, { format: 'raw' });
        });

        // Check page uses id="submit" or name="submit" for it's submit button
        if (settings.submit_patch && !form.submit.nodeType && !form.submit.length && !form._mceOldSubmit) {
          form._mceOldSubmit = form.submit;
          form.submit = function () {
            editor.editorManager.triggerSave();
            editor.setDirty(false);

            return form._mceOldSubmit(form);
          };
        }
      }

      editor.windowManager = new WindowManager(editor);
      editor.notificationManager = new NotificationManager(editor);

      if (settings.encoding === 'xml') {
        editor.on('GetContent', function (e) {
          if (e.save) {
            e.content = DOM.encode(e.content);
          }
        });
      }

      if (settings.add_form_submit_trigger) {
        editor.on('submit', function () {
          if (editor.initialized) {
            editor.save();
          }
        });
      }

      if (settings.add_unload_trigger) {
        editor._beforeUnload = function () {
          if (editor.initialized && !editor.destroyed && !editor.isHidden()) {
            editor.save({ format: 'raw', no_events: true, set_dirty: false });
          }
        };

        editor.editorManager.on('BeforeUnload', editor._beforeUnload);
      }

      editor.editorManager.add(editor);
      loadScripts(editor, editor.suffix);
    };

    return {
      render: render
    };
  }
);

/**
 * Mode.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
 */

/**
 * Mode switcher logic.
 *
 * @private
 * @class tinymce.Mode
 */
define(
  'tinymce.core.Mode',
  [
  ],
  function () {
    var setEditorCommandState = function (editor, cmd, state) {
      try {
        editor.getDoc().execCommand(cmd, false, state);
      } catch (ex) {
        // Ignore
      }
    };

    var clickBlocker = function (editor) {
      var target, handler;

      target = editor.getBody();

      handler = function (e) {
        if (editor.dom.getParents(e.target, 'a').length > 0) {
          e.preventDefault();
        }
      };

      editor.dom.bind(target, 'click', handler);

      return {
        unbind: function () {
          editor.dom.unbind(target, 'click', handler);
        }
      };
    };

    var toggleReadOnly = function (editor, state) {
      if (editor._clickBlocker) {
        editor._clickBlocker.unbind();
        editor._clickBlocker = null;
      }

      if (state) {
        editor._clickBlocker = clickBlocker(editor);
        editor.selection.controlSelection.hideResizeRect();
        editor.readonly = true;
        editor.getBody().contentEditable = false;
      } else {
        editor.readonly = false;
        editor.getBody().contentEditable = true;
        setEditorCommandState(editor, "StyleWithCSS", false);
        setEditorCommandState(editor, "enableInlineTableEditing", false);
        setEditorCommandState(editor, "enableObjectResizing", false);
        editor.focus();
        editor.nodeChanged();
      }
    };

    var setMode = function (editor, mode) {
      var currentMode = editor.readonly ? 'readonly' : 'design';

      if (mode == currentMode) {
        return;
      }

      if (editor.initialized) {
        toggleReadOnly(editor, mode == 'readonly');
      } else {
        editor.on('init', function () {
          toggleReadOnly(editor, mode == 'readonly');
        });
      }

      // Event is NOT preventable
      editor.fire('SwitchMode', { mode: mode });
    };

    return {
      setMode: setMode
    };
  }
);
/**
 * Shortcuts.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
 */

/**
 * Contains logic for handling keyboard shortcuts.
 *
 * @class tinymce.Shortcuts
 * @example
 * editor.shortcuts.add('ctrl+a', "description of the shortcut", function() {});
 * editor.shortcuts.add('meta+a', "description of the shortcut", function() {}); // "meta" maps to Command on Mac and Ctrl on PC
 * editor.shortcuts.add('ctrl+alt+a', "description of the shortcut", function() {});
 * editor.shortcuts.add('access+a', "description of the shortcut", function() {}); // "access" maps to ctrl+alt on Mac and shift+alt on PC
 */
define(
  'tinymce.core.Shortcuts',
  [
    'tinymce.core.util.Tools',
    'tinymce.core.Env'
  ],
  function (Tools, Env) {
    var each = Tools.each, explode = Tools.explode;

    var keyCodeLookup = {
      "f9": 120,
      "f10": 121,
      "f11": 122
    };

    var modifierNames = Tools.makeMap('alt,ctrl,shift,meta,access');

    return function (editor) {
      var self = this, shortcuts = {}, pendingPatterns = [];

      var parseShortcut = function (pattern) {
        var id, key, shortcut = {};

        // Parse modifiers and keys ctrl+alt+b for example
        each(explode(pattern, '+'), function (value) {
          if (value in modifierNames) {
            shortcut[value] = true;
          } else {
            // Allow numeric keycodes like ctrl+219 for ctrl+[
            if (/^[0-9]{2,}$/.test(value)) {
              shortcut.keyCode = parseInt(value, 10);
            } else {
              shortcut.charCode = value.charCodeAt(0);
              shortcut.keyCode = keyCodeLookup[value] || value.toUpperCase().charCodeAt(0);
            }
          }
        });

        // Generate unique id for modifier combination and set default state for unused modifiers
        id = [shortcut.keyCode];
        for (key in modifierNames) {
          if (shortcut[key]) {
            id.push(key);
          } else {
            shortcut[key] = false;
          }
        }
        shortcut.id = id.join(',');

        // Handle special access modifier differently depending on Mac/Win
        if (shortcut.access) {
          shortcut.alt = true;

          if (Env.mac) {
            shortcut.ctrl = true;
          } else {
            shortcut.shift = true;
          }
        }

        // Handle special meta modifier differently depending on Mac/Win
        if (shortcut.meta) {
          if (Env.mac) {
            shortcut.meta = true;
          } else {
            shortcut.ctrl = true;
            shortcut.meta = false;
          }
        }

        return shortcut;
      };

      var createShortcut = function (pattern, desc, cmdFunc, scope) {
        var shortcuts;

        shortcuts = Tools.map(explode(pattern, '>'), parseShortcut);
        shortcuts[shortcuts.length - 1] = Tools.extend(shortcuts[shortcuts.length - 1], {
          func: cmdFunc,
          scope: scope || editor
        });

        return Tools.extend(shortcuts[0], {
          desc: editor.translate(desc),
          subpatterns: shortcuts.slice(1)
        });
      };

      var hasModifier = function (e) {
        return e.altKey || e.ctrlKey || e.metaKey;
      };

      var isFunctionKey = function (e) {
        return e.type === "keydown" && e.keyCode >= 112 && e.keyCode <= 123;
      };

      var matchShortcut = function (e, shortcut) {
        if (!shortcut) {
          return false;
        }

        if (shortcut.ctrl != e.ctrlKey || shortcut.meta != e.metaKey) {
          return false;
        }

        if (shortcut.alt != e.altKey || shortcut.shift != e.shiftKey) {
          return false;
        }

        if (e.keyCode == shortcut.keyCode || (e.charCode && e.charCode == shortcut.charCode)) {
          e.preventDefault();
          return true;
        }

        return false;
      };

      var executeShortcutAction = function (shortcut) {
        return shortcut.func ? shortcut.func.call(shortcut.scope) : null;
      };

      editor.on('keyup keypress keydown', function (e) {
        if ((hasModifier(e) || isFunctionKey(e)) && !e.isDefaultPrevented()) {
          each(shortcuts, function (shortcut) {
            if (matchShortcut(e, shortcut)) {
              pendingPatterns = shortcut.subpatterns.slice(0);

              if (e.type == "keydown") {
                executeShortcutAction(shortcut);
              }

              return true;
            }
          });

          if (matchShortcut(e, pendingPatterns[0])) {
            if (pendingPatterns.length === 1) {
              if (e.type == "keydown") {
                executeShortcutAction(pendingPatterns[0]);
              }
            }

            pendingPatterns.shift();
          }
        }
      });

      /**
       * Adds a keyboard shortcut for some command or function.
       *
       * @method add
       * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o.
       * @param {String} desc Text description for the command.
       * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed.
       * @param {Object} scope Optional scope to execute the function in.
       * @return {Boolean} true/false state if the shortcut was added or not.
       */
      self.add = function (pattern, desc, cmdFunc, scope) {
        var cmd;

        cmd = cmdFunc;

        if (typeof cmdFunc === 'string') {
          cmdFunc = function () {
            editor.execCommand(cmd, false, null);
          };
        } else if (Tools.isArray(cmd)) {
          cmdFunc = function () {
            editor.execCommand(cmd[0], cmd[1], cmd[2]);
          };
        }

        each(explode(Tools.trim(pattern.toLowerCase())), function (pattern) {
          var shortcut = createShortcut(pattern, desc, cmdFunc, scope);
          shortcuts[shortcut.id] = shortcut;
        });

        return true;
      };

      /**
       * Remove a keyboard shortcut by pattern.
       *
       * @method remove
       * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o.
       * @return {Boolean} true/false state if the shortcut was removed or not.
       */
      self.remove = function (pattern) {
        var shortcut = createShortcut(pattern);

        if (shortcuts[shortcut.id]) {
          delete shortcuts[shortcut.id];
          return true;
        }

        return false;
      };
    };
  }
);

/**
 * Sidebar.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
 */

/**
 * This module handle sidebar instances for the editor.
 *
 * @class tinymce.ui.Sidebar
 * @private
 */
define(
  'tinymce.core.ui.Sidebar',
  [
  ],
  function (
  ) {
    var add = function (editor, name, settings) {
      var sidebars = editor.sidebars ? editor.sidebars : [];
      sidebars.push({ name: name, settings: settings });
      editor.sidebars = sidebars;
    };

    return {
      add: add
    };
  }
);

/**
 * URI.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
 */

/**
 * This class handles parsing, modification and serialization of URI/URL strings.
 * @class tinymce.util.URI
 */
define(
  'tinymce.core.util.URI',
  [
    'global!document',
    'tinymce.core.util.Tools'
  ],
  function (document, Tools) {
    var each = Tools.each, trim = Tools.trim;
    var queryParts = "source protocol authority userInfo user password host port relative path directory file query anchor".split(' ');
    var DEFAULT_PORTS = {
      'ftp': 21,
      'http': 80,
      'https': 443,
      'mailto': 25
    };

    /**
     * Constructs a new URI instance.
     *
     * @constructor
     * @method URI
     * @param {String} url URI string to parse.
     * @param {Object} settings Optional settings object.
     */
    var URI = function (url, settings) {
      var self = this, baseUri, baseUrl;

      url = trim(url);
      settings = self.settings = settings || {};
      baseUri = settings.base_uri;

      // Strange app protocol that isn't http/https or local anchor
      // For example: mailto,skype,tel etc.
      if (/^([\w\-]+):([^\/]{2})/i.test(url) || /^\s*#/.test(url)) {
        self.source = url;
        return;
      }

      var isProtocolRelative = url.indexOf('//') === 0;

      // Absolute path with no host, fake host and protocol
      if (url.indexOf('/') === 0 && !isProtocolRelative) {
        url = (baseUri ? baseUri.protocol || 'http' : 'http') + '://mce_host' + url;
      }

      // Relative path http:// or protocol relative //path
      if (!/^[\w\-]*:?\/\//.test(url)) {
        baseUrl = settings.base_uri ? settings.base_uri.path : new URI(document.location.href).directory;
        if (settings.base_uri.protocol === "") {
          url = '//mce_host' + self.toAbsPath(baseUrl, url);
        } else {
          url = /([^#?]*)([#?]?.*)/.exec(url);
          url = ((baseUri && baseUri.protocol) || 'http') + '://mce_host' + self.toAbsPath(baseUrl, url[1]) + url[2];
        }
      }

      // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri)
      url = url.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something

      /*jshint maxlen: 255 */
      /*eslint max-len: 0 */
      url = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(url);

      each(queryParts, function (v, i) {
        var part = url[i];

        // Zope 3 workaround, they use @@something
        if (part) {
          part = part.replace(/\(mce_at\)/g, '@@');
        }

        self[v] = part;
      });

      if (baseUri) {
        if (!self.protocol) {
          self.protocol = baseUri.protocol;
        }

        if (!self.userInfo) {
          self.userInfo = baseUri.userInfo;
        }

        if (!self.port && self.host === 'mce_host') {
          self.port = baseUri.port;
        }

        if (!self.host || self.host === 'mce_host') {
          self.host = baseUri.host;
        }

        self.source = '';
      }

      if (isProtocolRelative) {
        self.protocol = '';
      }

      //t.path = t.path || '/';
    };

    URI.prototype = {
      /**
       * Sets the internal path part of the URI.
       *
       * @method setPath
       * @param {string} path Path string to set.
       */
      setPath: function (path) {
        var self = this;

        path = /^(.*?)\/?(\w+)?$/.exec(path);

        // Update path parts
        self.path = path[0];
        self.directory = path[1];
        self.file = path[2];

        // Rebuild source
        self.source = '';
        self.getURI();
      },

      /**
       * Converts the specified URI into a relative URI based on the current URI instance location.
       *
       * @method toRelative
       * @param {String} uri URI to convert into a relative path/URI.
       * @return {String} Relative URI from the point specified in the current URI instance.
       * @example
       * // Converts an absolute URL to an relative URL url will be somedir/somefile.htm
       * var url = new tinymce.util.URI('http://www.site.com/dir/').toRelative('http://www.site.com/dir/somedir/somefile.htm');
       */
      toRelative: function (uri) {
        var self = this, output;

        if (uri === "./") {
          return uri;
        }

        uri = new URI(uri, { base_uri: self });

        // Not on same domain/port or protocol
        if ((uri.host != 'mce_host' && self.host != uri.host && uri.host) || self.port != uri.port ||
          (self.protocol != uri.protocol && uri.protocol !== "")) {
          return uri.getURI();
        }

        var tu = self.getURI(), uu = uri.getURI();

        // Allow usage of the base_uri when relative_urls = true
        if (tu == uu || (tu.charAt(tu.length - 1) == "/" && tu.substr(0, tu.length - 1) == uu)) {
          return tu;
        }

        output = self.toRelPath(self.path, uri.path);

        // Add query
        if (uri.query) {
          output += '?' + uri.query;
        }

        // Add anchor
        if (uri.anchor) {
          output += '#' + uri.anchor;
        }

        return output;
      },

      /**
       * Converts the specified URI into a absolute URI based on the current URI instance location.
       *
       * @method toAbsolute
       * @param {String} uri URI to convert into a relative path/URI.
       * @param {Boolean} noHost No host and protocol prefix.
       * @return {String} Absolute URI from the point specified in the current URI instance.
       * @example
       * // Converts an relative URL to an absolute URL url will be http://www.site.com/dir/somedir/somefile.htm
       * var url = new tinymce.util.URI('http://www.site.com/dir/').toAbsolute('somedir/somefile.htm');
       */
      toAbsolute: function (uri, noHost) {
        uri = new URI(uri, { base_uri: this });

        return uri.getURI(noHost && this.isSameOrigin(uri));
      },

      /**
       * Determine whether the given URI has the same origin as this URI.  Based on RFC-6454.
       * Supports default ports for protocols listed in DEFAULT_PORTS.  Unsupported protocols will fail safe: they
       * won't match, if the port specifications differ.
       *
       * @method isSameOrigin
       * @param {tinymce.util.URI} uri Uri instance to compare.
       * @returns {Boolean} True if the origins are the same.
       */
      isSameOrigin: function (uri) {
        if (this.host == uri.host && this.protocol == uri.protocol) {
          if (this.port == uri.port) {
            return true;
          }

          var defaultPort = DEFAULT_PORTS[this.protocol];
          if (defaultPort && ((this.port || defaultPort) == (uri.port || defaultPort))) {
            return true;
          }
        }

        return false;
      },

      /**
       * Converts a absolute path into a relative path.
       *
       * @method toRelPath
       * @param {String} base Base point to convert the path from.
       * @param {String} path Absolute path to convert into a relative path.
       */
      toRelPath: function (base, path) {
        var items, breakPoint = 0, out = '', i, l;

        // Split the paths
        base = base.substring(0, base.lastIndexOf('/'));
        base = base.split('/');
        items = path.split('/');

        if (base.length >= items.length) {
          for (i = 0, l = base.length; i < l; i++) {
            if (i >= items.length || base[i] != items[i]) {
              breakPoint = i + 1;
              break;
            }
          }
        }

        if (base.length < items.length) {
          for (i = 0, l = items.length; i < l; i++) {
            if (i >= base.length || base[i] != items[i]) {
              breakPoint = i + 1;
              break;
            }
          }
        }

        if (breakPoint === 1) {
          return path;
        }

        for (i = 0, l = base.length - (breakPoint - 1); i < l; i++) {
          out += "../";
        }

        for (i = breakPoint - 1, l = items.length; i < l; i++) {
          if (i != breakPoint - 1) {
            out += "/" + items[i];
          } else {
            out += items[i];
          }
        }

        return out;
      },

      /**
       * Converts a relative path into a absolute path.
       *
       * @method toAbsPath
       * @param {String} base Base point to convert the path from.
       * @param {String} path Relative path to convert into an absolute path.
       */
      toAbsPath: function (base, path) {
        var i, nb = 0, o = [], tr, outPath;

        // Split paths
        tr = /\/$/.test(path) ? '/' : '';
        base = base.split('/');
        path = path.split('/');

        // Remove empty chunks
        each(base, function (k) {
          if (k) {
            o.push(k);
          }
        });

        base = o;

        // Merge relURLParts chunks
        for (i = path.length - 1, o = []; i >= 0; i--) {
          // Ignore empty or .
          if (path[i].length === 0 || path[i] === ".") {
            continue;
          }

          // Is parent
          if (path[i] === '..') {
            nb++;
            continue;
          }

          // Move up
          if (nb > 0) {
            nb--;
            continue;
          }

          o.push(path[i]);
        }

        i = base.length - nb;

        // If /a/b/c or /
        if (i <= 0) {
          outPath = o.reverse().join('/');
        } else {
          outPath = base.slice(0, i).join('/') + '/' + o.reverse().join('/');
        }

        // Add front / if it's needed
        if (outPath.indexOf('/') !== 0) {
          outPath = '/' + outPath;
        }

        // Add traling / if it's needed
        if (tr && outPath.lastIndexOf('/') !== outPath.length - 1) {
          outPath += tr;
        }

        return outPath;
      },

      /**
       * Returns the full URI of the internal structure.
       *
       * @method getURI
       * @param {Boolean} noProtoHost Optional no host and protocol part. Defaults to false.
       */
      getURI: function (noProtoHost) {
        var s, self = this;

        // Rebuild source
        if (!self.source || noProtoHost) {
          s = '';

          if (!noProtoHost) {
            if (self.protocol) {
              s += self.protocol + '://';
            } else {
              s += '//';
            }

            if (self.userInfo) {
              s += self.userInfo + '@';
            }

            if (self.host) {
              s += self.host;
            }

            if (self.port) {
              s += ':' + self.port;
            }
          }

          if (self.path) {
            s += self.path;
          }

          if (self.query) {
            s += '?' + self.query;
          }

          if (self.anchor) {
            s += '#' + self.anchor;
          }

          self.source = s;
        }

        return self.source;
      }
    };

    URI.parseDataUri = function (uri) {
      var type, matches;

      uri = decodeURIComponent(uri).split(',');

      matches = /data:([^;]+)/.exec(uri[0]);
      if (matches) {
        type = matches[1];
      }

      return {
        type: type,
        data: uri[1]
      };
    };

    URI.getDocumentBaseUrl = function (loc) {
      var baseUrl;

      // Pass applewebdata:// and other non web protocols though
      if (loc.protocol.indexOf('http') !== 0 && loc.protocol !== 'file:') {
        baseUrl = loc.href;
      } else {
        baseUrl = loc.protocol + '//' + loc.host + loc.pathname;
      }

      if (/^[^:]+:\/\/\/?[^\/]+\//.test(baseUrl)) {
        baseUrl = baseUrl.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');

        if (!/[\/\\]$/.test(baseUrl)) {
          baseUrl += '/';
        }
      }

      return baseUrl;
    };

    return URI;
  }
);

/**
 * Editor.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
 */

/*jshint scripturl:true */

/**
 * Include the base event class documentation.
 *
 * @include ../../../../../tools/docs/tinymce.Event.js
 */

/**
 * This class contains the core logic for a TinyMCE editor.
 *
 * @class tinymce.Editor
 * @mixes tinymce.util.Observable
 * @example
 * // Add a class to all paragraphs in the editor.
 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass');
 *
 * // Gets the current editors selection as text
 * tinymce.activeEditor.selection.getContent({format: 'text'});
 *
 * // Creates a new editor instance
 * var ed = new tinymce.Editor('textareaid', {
 *     some_setting: 1
 * }, tinymce.EditorManager);
 *
 * ed.render();
 */
define(
  'tinymce.core.Editor',
  [
    'tinymce.core.AddOnManager',
    'tinymce.core.dom.DomQuery',
    'tinymce.core.dom.DOMUtils',
    'tinymce.core.EditorCommands',
    'tinymce.core.EditorFocus',
    'tinymce.core.EditorObservable',
    'tinymce.core.EditorSettings',
    'tinymce.core.Env',
    'tinymce.core.html.Serializer',
    'tinymce.core.init.Render',
    'tinymce.core.Mode',
    'tinymce.core.Shortcuts',
    'tinymce.core.ui.Sidebar',
    'tinymce.core.util.Tools',
    'tinymce.core.util.URI',
    'tinymce.core.util.Uuid'
  ],
  function (
    AddOnManager, DomQuery, DOMUtils, EditorCommands, EditorFocus, EditorObservable, EditorSettings, Env, Serializer, Render, Mode, Shortcuts, Sidebar, Tools,
    URI, Uuid
  ) {
    // Shorten these names
    var DOM = DOMUtils.DOM;
    var extend = Tools.extend, each = Tools.each;
    var trim = Tools.trim, resolve = Tools.resolve;
    var ie = Env.ie;

    /**
     * Include Editor API docs.
     *
     * @include ../../../../../tools/docs/tinymce.Editor.js
     */

    /**
     * Constructs a editor instance by id.
     *
     * @constructor
     * @method Editor
     * @param {String} id Unique id for the editor.
     * @param {Object} settings Settings for the editor.
     * @param {tinymce.EditorManager} editorManager EditorManager instance.
     */
    var Editor = function (id, settings, editorManager) {
      var self = this, documentBaseUrl, baseUri;

      documentBaseUrl = self.documentBaseUrl = editorManager.documentBaseURL;
      baseUri = editorManager.baseURI;

      /**
       * Name/value collection with editor settings.
       *
       * @property settings
       * @type Object
       * @example
       * // Get the value of the theme setting
       * tinymce.activeEditor.windowManager.alert("You are using the " + tinymce.activeEditor.settings.theme + " theme");
       */
      settings = EditorSettings.getEditorSettings(self, id, documentBaseUrl, editorManager.defaultSettings, settings);
      self.settings = settings;

      AddOnManager.language = settings.language || 'en';
      AddOnManager.languageLoad = settings.language_load;
      AddOnManager.baseURL = editorManager.baseURL;

      /**
       * Editor instance id, normally the same as the div/textarea that was replaced.
       *
       * @property id
       * @type String
       */
      self.id = id;

      /**
       * State to force the editor to return false on a isDirty call.
       *
       * @property isNotDirty
       * @type Boolean
       * @deprecated Use editor.setDirty instead.
       */
      self.setDirty(false);

      /**
       * Name/Value object containing plugin instances.
       *
       * @property plugins
       * @type Object
       * @example
       * // Execute a method inside a plugin directly
       * tinymce.activeEditor.plugins.someplugin.someMethod();
       */
      self.plugins = {};

      /**
       * URI object to document configured for the TinyMCE instance.
       *
       * @property documentBaseURI
       * @type tinymce.util.URI
       * @example
       * // Get relative URL from the location of document_base_url
       * tinymce.activeEditor.documentBaseURI.toRelative('/somedir/somefile.htm');
       *
       * // Get absolute URL from the location of document_base_url
       * tinymce.activeEditor.documentBaseURI.toAbsolute('somefile.htm');
       */
      self.documentBaseURI = new URI(settings.document_base_url, {
        base_uri: baseUri
      });

      /**
       * URI object to current document that holds the TinyMCE editor instance.
       *
       * @property baseURI
       * @type tinymce.util.URI
       * @example
       * // Get relative URL from the location of the API
       * tinymce.activeEditor.baseURI.toRelative('/somedir/somefile.htm');
       *
       * // Get absolute URL from the location of the API
       * tinymce.activeEditor.baseURI.toAbsolute('somefile.htm');
       */
      self.baseURI = baseUri;

      /**
       * Array with CSS files to load into the iframe.
       *
       * @property contentCSS
       * @type Array
       */
      self.contentCSS = [];

      /**
       * Array of CSS styles to add to head of document when the editor loads.
       *
       * @property contentStyles
       * @type Array
       */
      self.contentStyles = [];

      self.shortcuts = new Shortcuts(self);
      self.loadedCSS = {};
      self.editorCommands = new EditorCommands(self);
      self.suffix = editorManager.suffix;
      self.editorManager = editorManager;
      self.inline = settings.inline;
      self.buttons = {};
      self.menuItems = {};

      if (settings.cache_suffix) {
        Env.cacheSuffix = settings.cache_suffix.replace(/^[\?\&]+/, '');
      }

      if (settings.override_viewport === false) {
        Env.overrideViewPort = false;
      }

      // Call setup
      editorManager.fire('SetupEditor', self);
      self.execCallback('setup', self);

      /**
       * Dom query instance with default scope to the editor document and default element is the body of the editor.
       *
       * @property $
       * @type tinymce.dom.DomQuery
       * @example
       * tinymce.activeEditor.$('p').css('color', 'red');
       * tinymce.activeEditor.$().append('<p>new</p>');
       */
      self.$ = DomQuery.overrideDefaults(function () {
        return {
          context: self.inline ? self.getBody() : self.getDoc(),
          element: self.getBody()
        };
      });
    };

    Editor.prototype = {
      /**
       * Renders the editor/adds it to the page.
       *
       * @method render
       */
      render: function () {
        Render.render(this);
      },

      /**
       * Focuses/activates the editor. This will set this editor as the activeEditor in the tinymce collection
       * it will also place DOM focus inside the editor.
       *
       * @method focus
       * @param {Boolean} skipFocus Skip DOM focus. Just set is as the active editor.
       */
      focus: function (skipFocus) {
        EditorFocus.focus(this, skipFocus);
      },

      /**
       * Executes a legacy callback. This method is useful to call old 2.x option callbacks.
       * There new event model is a better way to add callback so this method might be removed in the future.
       *
       * @method execCallback
       * @param {String} name Name of the callback to execute.
       * @return {Object} Return value passed from callback function.
       */
      execCallback: function (name) {
        var self = this, callback = self.settings[name], scope;

        if (!callback) {
          return;
        }

        // Look through lookup
        if (self.callbackLookup && (scope = self.callbackLookup[name])) {
          callback = scope.func;
          scope = scope.scope;
        }

        if (typeof callback === 'string') {
          scope = callback.replace(/\.\w+$/, '');
          scope = scope ? resolve(scope) : 0;
          callback = resolve(callback);
          self.callbackLookup = self.callbackLookup || {};
          self.callbackLookup[name] = { func: callback, scope: scope };
        }

        return callback.apply(scope || self, Array.prototype.slice.call(arguments, 1));
      },

      /**
       * Translates the specified string by replacing variables with language pack items it will also check if there is
       * a key matching the input.
       *
       * @method translate
       * @param {String} text String to translate by the language pack data.
       * @return {String} Translated string.
       */
      translate: function (text) {
        if (text && Tools.is(text, 'string')) {
          var lang = this.settings.language || 'en', i18n = this.editorManager.i18n;

          text = i18n.data[lang + '.' + text] || text.replace(/\{\#([^\}]+)\}/g, function (a, b) {
            return i18n.data[lang + '.' + b] || '{#' + b + '}';
          });
        }

        return this.editorManager.translate(text);
      },

      /**
       * Returns a language pack item by name/key.
       *
       * @method getLang
       * @param {String} name Name/key to get from the language pack.
       * @param {String} defaultVal Optional default value to retrieve.
       */
      getLang: function (name, defaultVal) {
        return (
          this.editorManager.i18n.data[(this.settings.language || 'en') + '.' + name] ||
          (defaultVal !== undefined ? defaultVal : '{#' + name + '}')
        );
      },

      /**
       * Returns a configuration parameter by name.
       *
       * @method getParam
       * @param {String} name Configruation parameter to retrieve.
       * @param {String} defaultVal Optional default value to return.
       * @param {String} type Optional type parameter.
       * @return {String} Configuration parameter value or default value.
       * @example
       * // Returns a specific config value from the currently active editor
       * var someval = tinymce.activeEditor.getParam('myvalue');
       *
       * // Returns a specific config value from a specific editor instance by id
       * var someval2 = tinymce.get('my_editor').getParam('myvalue');
       */
      getParam: function (name, defaultVal, type) {
        var value = name in this.settings ? this.settings[name] : defaultVal, output;

        if (type === 'hash') {
          output = {};

          if (typeof value === 'string') {
            each(value.indexOf('=') > 0 ? value.split(/[;,](?![^=;,]*(?:[;,]|$))/) : value.split(','), function (value) {
              value = value.split('=');

              if (value.length > 1) {
                output[trim(value[0])] = trim(value[1]);
              } else {
                output[trim(value[0])] = trim(value);
              }
            });
          } else {
            output = value;
          }

          return output;
        }

        return value;
      },

      /**
       * Dispatches out a onNodeChange event to all observers. This method should be called when you
       * need to update the UI states or element path etc.
       *
       * @method nodeChanged
       * @param {Object} args Optional args to pass to NodeChange event handlers.
       */
      nodeChanged: function (args) {
        this._nodeChangeDispatcher.nodeChanged(args);
      },

      /**
       * Adds a button that later gets created by the theme in the editors toolbars.
       *
       * @method addButton
       * @param {String} name Button name to add.
       * @param {Object} settings Settings object with title, cmd etc.
       * @example
       * // Adds a custom button to the editor that inserts contents when clicked
       * tinymce.init({
       *    ...
       *
       *    toolbar: 'example'
       *
       *    setup: function(ed) {
       *       ed.addButton('example', {
       *          title: 'My title',
       *          image: '../js/tinymce/plugins/example/img/example.gif',
       *          onclick: function() {
       *             ed.insertContent('Hello world!!');
       *          }
       *       });
       *    }
       * });
       */
      addButton: function (name, settings) {
        var self = this;

        if (settings.cmd) {
          settings.onclick = function () {
            self.execCommand(settings.cmd);
          };
        }

        if (!settings.text && !settings.icon) {
          settings.icon = name;
        }

        self.buttons = self.buttons;
        settings.tooltip = settings.tooltip || settings.title;
        self.buttons[name] = settings;
      },

      /**
       * Adds a sidebar for the editor instance.
       *
       * @method addSidebar
       * @param {String} name Sidebar name to add.
       * @param {Object} settings Settings object with icon, onshow etc.
       * @example
       * // Adds a custom sidebar that when clicked logs the panel element
       * tinymce.init({
       *    ...
       *    setup: function(ed) {
       *       ed.addSidebar('example', {
       *          tooltip: 'My sidebar',
       *          icon: 'my-side-bar',
       *          onshow: function(api) {
       *             console.log(api.element());
       *          }
       *       });
       *    }
       * });
       */
      addSidebar: function (name, settings) {
        return Sidebar.add(this, name, settings);
      },

      /**
       * Adds a menu item to be used in the menus of the theme. There might be multiple instances
       * of this menu item for example it might be used in the main menus of the theme but also in
       * the context menu so make sure that it's self contained and supports multiple instances.
       *
       * @method addMenuItem
       * @param {String} name Menu item name to add.
       * @param {Object} settings Settings object with title, cmd etc.
       * @example
       * // Adds a custom menu item to the editor that inserts contents when clicked
       * // The context option allows you to add the menu item to an existing default menu
       * tinymce.init({
       *    ...
       *
       *    setup: function(ed) {
       *       ed.addMenuItem('example', {
       *          text: 'My menu item',
       *          context: 'tools',
       *          onclick: function() {
       *             ed.insertContent('Hello world!!');
       *          }
       *       });
       *    }
       * });
       */
      addMenuItem: function (name, settings) {
        var self = this;

        if (settings.cmd) {
          settings.onclick = function () {
            self.execCommand(settings.cmd);
          };
        }

        self.menuItems = self.menuItems;
        self.menuItems[name] = settings;
      },

      /**
       * Adds a contextual toolbar to be rendered when the selector matches.
       *
       * @method addContextToolbar
       * @param {function/string} predicate Predicate that needs to return true if provided strings get converted into CSS predicates.
       * @param {String/Array} items String or array with items to add to the context toolbar.
       */
      addContextToolbar: function (predicate, items) {
        var self = this, selector;

        self.contextToolbars = self.contextToolbars || [];

        // Convert selector to predicate
        if (typeof predicate == "string") {
          selector = predicate;
          predicate = function (elm) {
            return self.dom.is(elm, selector);
          };
        }

        self.contextToolbars.push({
          id: Uuid.uuid('mcet'),
          predicate: predicate,
          items: items
        });
      },

      /**
       * Adds a custom command to the editor, you can also override existing commands with this method.
       * The command that you add can be executed with execCommand.
       *
       * @method addCommand
       * @param {String} name Command name to add/override.
       * @param {addCommandCallback} callback Function to execute when the command occurs.
       * @param {Object} scope Optional scope to execute the function in.
       * @example
       * // Adds a custom command that later can be executed using execCommand
       * tinymce.init({
       *    ...
       *
       *    setup: function(ed) {
       *       // Register example command
       *       ed.addCommand('mycommand', function(ui, v) {
       *          ed.windowManager.alert('Hello world!! Selection: ' + ed.selection.getContent({format: 'text'}));
       *       });
       *    }
       * });
       */
      addCommand: function (name, callback, scope) {
        /**
         * Callback function that gets called when a command is executed.
         *
         * @callback addCommandCallback
         * @param {Boolean} ui Display UI state true/false.
         * @param {Object} value Optional value for command.
         * @return {Boolean} True/false state if the command was handled or not.
         */
        this.editorCommands.addCommand(name, callback, scope);
      },

      /**
       * Adds a custom query state command to the editor, you can also override existing commands with this method.
       * The command that you add can be executed with queryCommandState function.
       *
       * @method addQueryStateHandler
       * @param {String} name Command name to add/override.
       * @param {addQueryStateHandlerCallback} callback Function to execute when the command state retrieval occurs.
       * @param {Object} scope Optional scope to execute the function in.
       */
      addQueryStateHandler: function (name, callback, scope) {
        /**
         * Callback function that gets called when a queryCommandState is executed.
         *
         * @callback addQueryStateHandlerCallback
         * @return {Boolean} True/false state if the command is enabled or not like is it bold.
         */
        this.editorCommands.addQueryStateHandler(name, callback, scope);
      },

      /**
       * Adds a custom query value command to the editor, you can also override existing commands with this method.
       * The command that you add can be executed with queryCommandValue function.
       *
       * @method addQueryValueHandler
       * @param {String} name Command name to add/override.
       * @param {addQueryValueHandlerCallback} callback Function to execute when the command value retrieval occurs.
       * @param {Object} scope Optional scope to execute the function in.
       */
      addQueryValueHandler: function (name, callback, scope) {
        /**
         * Callback function that gets called when a queryCommandValue is executed.
         *
         * @callback addQueryValueHandlerCallback
         * @return {Object} Value of the command or undefined.
         */
        this.editorCommands.addQueryValueHandler(name, callback, scope);
      },

      /**
       * Adds a keyboard shortcut for some command or function.
       *
       * @method addShortcut
       * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o.
       * @param {String} desc Text description for the command.
       * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed.
       * @param {Object} sc Optional scope to execute the function in.
       * @return {Boolean} true/false state if the shortcut was added or not.
       */
      addShortcut: function (pattern, desc, cmdFunc, scope) {
        this.shortcuts.add(pattern, desc, cmdFunc, scope);
      },

      /**
       * Executes a command on the current instance. These commands can be TinyMCE internal commands prefixed with "mce" or
       * they can be build in browser commands such as "Bold". A compleate list of browser commands is available on MSDN or Mozilla.org.
       * This function will dispatch the execCommand function on each plugin, theme or the execcommand_callback option if none of these
       * return true it will handle the command as a internal browser command.
       *
       * @method execCommand
       * @param {String} cmd Command name to execute, for example mceLink or Bold.
       * @param {Boolean} ui True/false state if a UI (dialog) should be presented or not.
       * @param {mixed} value Optional command value, this can be anything.
       * @param {Object} args Optional arguments object.
       */
      execCommand: function (cmd, ui, value, args) {
        return this.editorCommands.execCommand(cmd, ui, value, args);
      },

      /**
       * Returns a command specific state, for example if bold is enabled or not.
       *
       * @method queryCommandState
       * @param {string} cmd Command to query state from.
       * @return {Boolean} Command specific state, for example if bold is enabled or not.
       */
      queryCommandState: function (cmd) {
        return this.editorCommands.queryCommandState(cmd);
      },

      /**
       * Returns a command specific value, for example the current font size.
       *
       * @method queryCommandValue
       * @param {string} cmd Command to query value from.
       * @return {Object} Command specific value, for example the current font size.
       */
      queryCommandValue: function (cmd) {
        return this.editorCommands.queryCommandValue(cmd);
      },

      /**
       * Returns true/false if the command is supported or not.
       *
       * @method queryCommandSupported
       * @param {String} cmd Command that we check support for.
       * @return {Boolean} true/false if the command is supported or not.
       */
      queryCommandSupported: function (cmd) {
        return this.editorCommands.queryCommandSupported(cmd);
      },

      /**
       * Shows the editor and hides any textarea/div that the editor is supposed to replace.
       *
       * @method show
       */
      show: function () {
        var self = this;

        if (self.hidden) {
          self.hidden = false;

          if (self.inline) {
            self.getBody().contentEditable = true;
          } else {
            DOM.show(self.getContainer());
            DOM.hide(self.id);
          }

          self.load();
          self.fire('show');
        }
      },

      /**
       * Hides the editor and shows any textarea/div that the editor is supposed to replace.
       *
       * @method hide
       */
      hide: function () {
        var self = this, doc = self.getDoc();

        if (!self.hidden) {
          // Fixed bug where IE has a blinking cursor left from the editor
          if (ie && doc && !self.inline) {
            doc.execCommand('SelectAll');
          }

          // We must save before we hide so Safari doesn't crash
          self.save();

          if (self.inline) {
            self.getBody().contentEditable = false;

            // Make sure the editor gets blurred
            if (self == self.editorManager.focusedEditor) {
              self.editorManager.focusedEditor = null;
            }
          } else {
            DOM.hide(self.getContainer());
            DOM.setStyle(self.id, 'display', self.orgDisplay);
          }

          self.hidden = true;
          self.fire('hide');
        }
      },

      /**
       * Returns true/false if the editor is hidden or not.
       *
       * @method isHidden
       * @return {Boolean} True/false if the editor is hidden or not.
       */
      isHidden: function () {
        return !!this.hidden;
      },

      /**
       * Sets the progress state, this will display a throbber/progess for the editor.
       * This is ideal for asynchronous operations like an AJAX save call.
       *
       * @method setProgressState
       * @param {Boolean} state Boolean state if the progress should be shown or hidden.
       * @param {Number} time Optional time to wait before the progress gets shown.
       * @return {Boolean} Same as the input state.
       * @example
       * // Show progress for the active editor
       * tinymce.activeEditor.setProgressState(true);
       *
       * // Hide progress for the active editor
       * tinymce.activeEditor.setProgressState(false);
       *
       * // Show progress after 3 seconds
       * tinymce.activeEditor.setProgressState(true, 3000);
       */
      setProgressState: function (state, time) {
        this.fire('ProgressState', { state: state, time: time });
      },

      /**
       * Loads contents from the textarea or div element that got converted into an editor instance.
       * This method will move the contents from that textarea or div into the editor by using setContent
       * so all events etc that method has will get dispatched as well.
       *
       * @method load
       * @param {Object} args Optional content object, this gets passed around through the whole load process.
       * @return {String} HTML string that got set into the editor.
       */
      load: function (args) {
        var self = this, elm = self.getElement(), html;

        if (self.removed) {
          return '';
        }

        if (elm) {
          args = args || {};
          args.load = true;

          html = self.setContent(elm.value !== undefined ? elm.value : elm.innerHTML, args);
          args.element = elm;

          if (!args.no_events) {
            self.fire('LoadContent', args);
          }

          args.element = elm = null;

          return html;
        }
      },

      /**
       * Saves the contents from a editor out to the textarea or div element that got converted into an editor instance.
       * This method will move the HTML contents from the editor into that textarea or div by getContent
       * so all events etc that method has will get dispatched as well.
       *
       * @method save
       * @param {Object} args Optional content object, this gets passed around through the whole save process.
       * @return {String} HTML string that got set into the textarea/div.
       */
      save: function (args) {
        var self = this, elm = self.getElement(), html, form;

        if (!elm || !self.initialized || self.removed) {
          return;
        }

        args = args || {};
        args.save = true;

        args.element = elm;
        html = args.content = self.getContent(args);

        if (!args.no_events) {
          self.fire('SaveContent', args);
        }

        // Always run this internal event
        if (args.format == 'raw') {
          self.fire('RawSaveContent', args);
        }

        html = args.content;

        if (!/TEXTAREA|INPUT/i.test(elm.nodeName)) {
          // Update DIV element when not in inline mode
          if (!self.inline) {
            elm.innerHTML = html;
          }

          // Update hidden form element
          if ((form = DOM.getParent(self.id, 'form'))) {
            each(form.elements, function (elm) {
              if (elm.name == self.id) {
                elm.value = html;
                return false;
              }
            });
          }
        } else {
          elm.value = html;
        }

        args.element = elm = null;

        if (args.set_dirty !== false) {
          self.setDirty(false);
        }

        return html;
      },

      /**
       * Sets the specified content to the editor instance, this will cleanup the content before it gets set using
       * the different cleanup rules options.
       *
       * @method setContent
       * @param {String} content Content to set to editor, normally HTML contents but can be other formats as well.
       * @param {Object} args Optional content object, this gets passed around through the whole set process.
       * @return {String} HTML string that got set into the editor.
       * @example
       * // Sets the HTML contents of the activeEditor editor
       * tinymce.activeEditor.setContent('<span>some</span> html');
       *
       * // Sets the raw contents of the activeEditor editor
       * tinymce.activeEditor.setContent('<span>some</span> html', {format: 'raw'});
       *
       * // Sets the content of a specific editor (my_editor in this example)
       * tinymce.get('my_editor').setContent(data);
       *
       * // Sets the bbcode contents of the activeEditor editor if the bbcode plugin was added
       * tinymce.activeEditor.setContent('[b]some[/b] html', {format: 'bbcode'});
       */
      setContent: function (content, args) {
        var self = this, body = self.getBody(), forcedRootBlockName, padd;

        // Setup args object
        args = args || {};
        args.format = args.format || 'html';
        args.set = true;
        args.content = content;

        // Do preprocessing
        if (!args.no_events) {
          self.fire('BeforeSetContent', args);
        }

        content = args.content;

        // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content
        // It will also be impossible to place the caret in the editor unless there is a BR element present
        if (content.length === 0 || /^\s+$/.test(content)) {
          padd = ie && ie < 11 ? '' : '<br data-mce-bogus="1">';

          // Todo: There is a lot more root elements that need special padding
          // so separate this and add all of them at some point.
          if (body.nodeName == 'TABLE') {
            content = '<tr><td>' + padd + '</td></tr>';
          } else if (/^(UL|OL)$/.test(body.nodeName)) {
            content = '<li>' + padd + '</li>';
          }

          forcedRootBlockName = self.settings.forced_root_block;

          // Check if forcedRootBlock is configured and that the block is a valid child of the body
          if (forcedRootBlockName && self.schema.isValidChild(body.nodeName.toLowerCase(), forcedRootBlockName.toLowerCase())) {
            // Padd with bogus BR elements on modern browsers and IE 7 and 8 since they don't render empty P tags properly
            content = padd;
            content = self.dom.createHTML(forcedRootBlockName, self.settings.forced_root_block_attrs, content);
          } else if (!ie && !content) {
            // We need to add a BR when forced_root_block is disabled on non IE browsers to place the caret
            content = '<br data-mce-bogus="1">';
          }

          self.dom.setHTML(body, content);

          self.fire('SetContent', args);
        } else {
          // Parse and serialize the html
          if (args.format !== 'raw') {
            content = new Serializer({
              validate: self.validate
            }, self.schema).serialize(
              self.parser.parse(content, { isRootContent: true })
              );
          }

          // Set the new cleaned contents to the editor
          args.content = trim(content);
          self.dom.setHTML(body, args.content);

          // Do post processing
          if (!args.no_events) {
            self.fire('SetContent', args);
          }

          // Don't normalize selection if the focused element isn't the body in
          // content editable mode since it will steal focus otherwise
          /*if (!self.settings.content_editable || document.activeElement === self.getBody()) {
            self.selection.normalize();
          }*/
        }

        return args.content;
      },

      /**
       * Gets the content from the editor instance, this will cleanup the content before it gets returned using
       * the different cleanup rules options.
       *
       * @method getContent
       * @param {Object} args Optional content object, this gets passed around through the whole get process.
       * @return {String} Cleaned content string, normally HTML contents.
       * @example
       * // Get the HTML contents of the currently active editor
       * console.debug(tinymce.activeEditor.getContent());
       *
       * // Get the raw contents of the currently active editor
       * tinymce.activeEditor.getContent({format: 'raw'});
       *
       * // Get content of a specific editor:
       * tinymce.get('content id').getContent()
       */
      getContent: function (args) {
        var self = this, content, body = self.getBody();

        if (self.removed) {
          return '';
        }

        // Setup args object
        args = args || {};
        args.format = args.format || 'html';
        args.get = true;
        args.getInner = true;

        // Do preprocessing
        if (!args.no_events) {
          self.fire('BeforeGetContent', args);
        }

        // Get raw contents or by default the cleaned contents
        if (args.format == 'raw') {
          content = Tools.trim(self.serializer.getTrimmedContent());
        } else if (args.format == 'text') {
          content = body.innerText || body.textContent;
        } else {
          content = self.serializer.serialize(body, args);
        }

        // Trim whitespace in beginning/end of HTML
        if (args.format != 'text') {
          args.content = trim(content);
        } else {
          args.content = content;
        }

        // Do post processing
        if (!args.no_events) {
          self.fire('GetContent', args);
        }

        return args.content;
      },

      /**
       * Inserts content at caret position.
       *
       * @method insertContent
       * @param {String} content Content to insert.
       * @param {Object} args Optional args to pass to insert call.
       */
      insertContent: function (content, args) {
        if (args) {
          content = extend({ content: content }, args);
        }

        this.execCommand('mceInsertContent', false, content);
      },

      /**
       * Returns true/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents.
       *
       * The dirty state is automatically set to true if you do modifications to the content in other
       * words when new undo levels is created or if you undo/redo to update the contents of the editor. It will also be set
       * to false if you call editor.save().
       *
       * @method isDirty
       * @return {Boolean} True/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents.
       * @example
       * if (tinymce.activeEditor.isDirty())
       *     alert("You must save your contents.");
       */
      isDirty: function () {
        return !this.isNotDirty;
      },

      /**
       * Explicitly sets the dirty state. This will fire the dirty event if the editor dirty state is changed from false to true
       * by invoking this method.
       *
       * @method setDirty
       * @param {Boolean} state True/false if the editor is considered dirty.
       * @example
       * function ajaxSave() {
       *     var editor = tinymce.get('elm1');
       *
       *     // Save contents using some XHR call
       *     alert(editor.getContent());
       *
       *     editor.setDirty(false); // Force not dirty state
       * }
       */
      setDirty: function (state) {
        var oldState = !this.isNotDirty;

        this.isNotDirty = !state;

        if (state && state != oldState) {
          this.fire('dirty');
        }
      },

      /**
       * Sets the editor mode. Mode can be for example "design", "code" or "readonly".
       *
       * @method setMode
       * @param {String} mode Mode to set the editor in.
       */
      setMode: function (mode) {
        Mode.setMode(this, mode);
      },

      /**
       * Returns the editors container element. The container element wrappes in
       * all the elements added to the page for the editor. Such as UI, iframe etc.
       *
       * @method getContainer
       * @return {Element} HTML DOM element for the editor container.
       */
      getContainer: function () {
        var self = this;

        if (!self.container) {
          self.container = DOM.get(self.editorContainer || self.id + '_parent');
        }

        return self.container;
      },

      /**
       * Returns the editors content area container element. The this element is the one who
       * holds the iframe or the editable element.
       *
       * @method getContentAreaContainer
       * @return {Element} HTML DOM element for the editor area container.
       */
      getContentAreaContainer: function () {
        return this.contentAreaContainer;
      },

      /**
       * Returns the target element/textarea that got replaced with a TinyMCE editor instance.
       *
       * @method getElement
       * @return {Element} HTML DOM element for the replaced element.
       */
      getElement: function () {
        if (!this.targetElm) {
          this.targetElm = DOM.get(this.id);
        }

        return this.targetElm;
      },

      /**
       * Returns the iframes window object.
       *
       * @method getWin
       * @return {Window} Iframe DOM window object.
       */
      getWin: function () {
        var self = this, elm;

        if (!self.contentWindow) {
          elm = self.iframeElement;

          if (elm) {
            self.contentWindow = elm.contentWindow;
          }
        }

        return self.contentWindow;
      },

      /**
       * Returns the iframes document object.
       *
       * @method getDoc
       * @return {Document} Iframe DOM document object.
       */
      getDoc: function () {
        var self = this, win;

        if (!self.contentDocument) {
          win = self.getWin();

          if (win) {
            self.contentDocument = win.document;
          }
        }

        return self.contentDocument;
      },

      /**
       * Returns the root element of the editable area.
       * For a non-inline iframe-based editor, returns the iframe's body element.
       *
       * @method getBody
       * @return {Element} The root element of the editable area.
       */
      getBody: function () {
        var doc = this.getDoc();
        return this.bodyElement || (doc ? doc.body : null);
      },

      /**
       * URL converter function this gets executed each time a user adds an img, a or
       * any other element that has a URL in it. This will be called both by the DOM and HTML
       * manipulation functions.
       *
       * @method convertURL
       * @param {string} url URL to convert.
       * @param {string} name Attribute name src, href etc.
       * @param {string/HTMLElement} elm Tag name or HTML DOM element depending on HTML or DOM insert.
       * @return {string} Converted URL string.
       */
      convertURL: function (url, name, elm) {
        var self = this, settings = self.settings;

        // Use callback instead
        if (settings.urlconverter_callback) {
          return self.execCallback('urlconverter_callback', url, elm, true, name);
        }

          // Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs
        if (!settings.convert_urls || (elm && elm.nodeName == 'LINK') || url.indexOf('file:') === 0 || url.length === 0) {
          return url;
        }

        // Convert to relative
        if (settings.relative_urls) {
          return self.documentBaseURI.toRelative(url);
        }
        // Convert to absolute
        url = self.documentBaseURI.toAbsolute(url, settings.remove_script_host);
        return url;
      },

      /**
       * Adds visual aid for tables, anchors etc so they can be more easily edited inside the editor.
       *
       * @method addVisual
       * @param {Element} elm Optional root element to loop though to find tables etc that needs the visual aid.
       */
      addVisual: function (elm) {
        var self = this, settings = self.settings, dom = self.dom, cls;

        elm = elm || self.getBody();

        if (self.hasVisual === undefined) {
          self.hasVisual = settings.visual;
        }

        each(dom.select('table,a', elm), function (elm) {
          var value;

          switch (elm.nodeName) {
            case 'TABLE':
              cls = settings.visual_table_class || 'mce-item-table';
              value = dom.getAttrib(elm, 'border');

              if ((!value || value == '0') && self.hasVisual) {
                dom.addClass(elm, cls);
              } else {
                dom.removeClass(elm, cls);
              }

              return;

            case 'A':
              if (!dom.getAttrib(elm, 'href', false)) {
                value = dom.getAttrib(elm, 'name') || elm.id;
                cls = settings.visual_anchor_class || 'mce-item-anchor';

                if (value && self.hasVisual) {
                  dom.addClass(elm, cls);
                } else {
                  dom.removeClass(elm, cls);
                }
              }

              return;
          }
        });

        self.fire('VisualAid', { element: elm, hasVisual: self.hasVisual });
      },

      /**
       * Removes the editor from the dom and tinymce collection.
       *
       * @method remove
       */
      remove: function () {
        var self = this;

        if (!self.removed) {
          self.save();
          self.removed = 1;
          self.unbindAllNativeEvents();

          // Remove any hidden input
          if (self.hasHiddenInput) {
            DOM.remove(self.getElement().nextSibling);
          }

          if (!self.inline) {
              // IE 9 has a bug where the selection stops working if you place the
              // caret inside the editor then remove the iframe
              if (ie && ie < 10) {
                  self.getDoc().execCommand('SelectAll', false, null);
              }

              DOM.setStyle(self.id, 'display', self.orgDisplay);
              if (self.getBody() != null && self.getBody().hasOwnProperty('onload'))
                  self.getBody().onload = null; // Prevent #6816
          }

          self.fire('remove');

          self.editorManager.remove(self);
          DOM.remove(self.getContainer());
          if (self._selectionOverrides != null && self._selectionOverrides.hasOwnProperty('destroy'))
          self._selectionOverrides.destroy();
          self.editorUpload.destroy();
          self.destroy();
        }
      },

      /**
       * Destroys the editor instance by removing all events, element references or other resources
       * that could leak memory. This method will be called automatically when the page is unloaded
       * but you can also call it directly if you know what you are doing.
       *
       * @method destroy
       * @param {Boolean} automatic Optional state if the destroy is an automatic destroy or user called one.
       */
      destroy: function (automatic) {
        var self = this, form;

        // One time is enough
        if (self.destroyed) {
          return;
        }

        // If user manually calls destroy and not remove
        // Users seems to have logic that calls destroy instead of remove
        if (!automatic && !self.removed) {
          self.remove();
          return;
        }

        if (!automatic) {
          self.editorManager.off('beforeunload', self._beforeUnload);

          // Manual destroy
          if (self.theme && self.theme.destroy) {
            self.theme.destroy();
          }

          // Destroy controls, selection and dom
          self.selection.destroy();
          self.dom.destroy();
        }

        form = self.formElement;
        if (form) {
          if (form._mceOldSubmit) {
            form.submit = form._mceOldSubmit;
            form._mceOldSubmit = null;
          }

          DOM.unbind(form, 'submit reset', self.formEventDelegate);
        }

        self.contentAreaContainer = self.formElement = self.container = self.editorContainer = null;
        self.bodyElement = self.contentDocument = self.contentWindow = null;
        self.iframeElement = self.targetElm = null;

        if (self.selection) {
          self.selection = self.selection.win = self.selection.dom = self.selection.dom.doc = null;
        }

        self.destroyed = 1;
      },

      /**
       * Uploads all data uri/blob uri images in the editor contents to server.
       *
       * @method uploadImages
       * @param {function} callback Optional callback with images and status for each image.
       * @return {tinymce.util.Promise} Promise instance.
       */
      uploadImages: function (callback) {
        return this.editorUpload.uploadImages(callback);
      },

      // Internal functions

      _scanForImages: function () {
        return this.editorUpload.scanForImages();
      }
    };

    extend(Editor.prototype, EditorObservable);

    return Editor;
  }
);

/**
 * FocusManager.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
 */

/**
 * This class manages the focus/blur state of the editor. This class is needed since some
 * browsers fire false focus/blur states when the selection is moved to a UI dialog or similar.
 *
 * This class will fire two events focus and blur on the editor instances that got affected.
 * It will also handle the restore of selection when the focus is lost and returned.
 *
 * @class tinymce.FocusManager
 */
define(
  'tinymce.core.FocusManager',
  [
    'ephox.sugar.api.node.Element',
    'global!document',
    'tinymce.core.dom.DOMUtils',
    'tinymce.core.selection.SelectionBookmark',
    'tinymce.core.util.Delay'
  ],
  function (Element, document, DOMUtils, SelectionBookmark, Delay) {
    var selectionChangeHandler, documentFocusInHandler, documentMouseUpHandler, DOM = DOMUtils.DOM;

    var isUIElement = function (editor, elm) {
      var customSelector = editor ? editor.settings.custom_ui_selector : '';
      var parent = DOM.getParent(elm, function (elm) {
        return (
          FocusManager.isEditorUIElement(elm) ||
          (customSelector ? editor.dom.is(elm, customSelector) : false)
        );
      });
      return parent !== null;
    };

    /**
     * Constructs a new focus manager instance.
     *
     * @constructor FocusManager
     * @param {tinymce.EditorManager} editorManager Editor manager instance to handle focus for.
     */
    var FocusManager = function (editorManager) {
      var getActiveElement = function () {
        try {
          return document.activeElement;
        } catch (ex) {
          // IE sometimes fails to get the activeElement when resizing table
          // TODO: Investigate this
          return document.body;
        }
      };

      var registerEvents = function (e) {
        var editor = e.editor;

        editor.on('init', function () {
          editor.on('keyup mouseup touchend nodechange', function (e) {
            if (e.type === 'nodechange' && e.selectionChange) {
              return;
            }
            SelectionBookmark.store(editor);
          });
        });

        editor.on('focusin', function () {
          var focusedEditor = editorManager.focusedEditor;

          if (focusedEditor != editor) {
            if (focusedEditor) {
              focusedEditor.fire('blur', { focusedEditor: editor });
            }

            editorManager.setActive(editor);
            editorManager.focusedEditor = editor;
            editor.fire('focus', { blurredEditor: focusedEditor });
            editor.focus(true);
          }
        });

        editor.on('focusout', function () {
          Delay.setEditorTimeout(editor, function () {
            var focusedEditor = editorManager.focusedEditor;

            // Still the same editor the blur was outside any editor UI
            if (!isUIElement(editor, getActiveElement()) && focusedEditor == editor) {
              editor.fire('blur', { focusedEditor: null });
              editorManager.focusedEditor = null;
            }
          });
        });

        // Check if focus is moved to an element outside the active editor by checking if the target node
        // isn't within the body of the activeEditor nor a UI element such as a dialog child control
        if (!documentFocusInHandler) {
          documentFocusInHandler = function (e) {
            var activeEditor = editorManager.activeEditor, target;

            target = e.target;

            if (activeEditor && target.ownerDocument === document) {
              // Fire a blur event if the element isn't a UI element
              if (target !== document.body && !isUIElement(activeEditor, target) && editorManager.focusedEditor === activeEditor) {
                activeEditor.fire('blur', { focusedEditor: null });
                editorManager.focusedEditor = null;
              }
            }
          };

          DOM.bind(document, 'focusin', documentFocusInHandler);
        }
      };

      var unregisterDocumentEvents = function (e) {
        if (editorManager.focusedEditor == e.editor) {
          editorManager.focusedEditor = null;
        }

        if (!editorManager.activeEditor) {
          DOM.unbind(document, 'selectionchange', selectionChangeHandler);
          DOM.unbind(document, 'focusin', documentFocusInHandler);
          DOM.unbind(document, 'mouseup', documentMouseUpHandler);
          selectionChangeHandler = documentFocusInHandler = documentMouseUpHandler = null;
        }
      };

      editorManager.on('AddEditor', registerEvents);
      editorManager.on('RemoveEditor', unregisterDocumentEvents);
    };

    /**
     * Returns true if the specified element is part of the UI for example an button or text input.
     *
     * @method isEditorUIElement
     * @param  {Element} elm Element to check if it's part of the UI or not.
     * @return {Boolean} True/false state if the element is part of the UI or not.
     */
    FocusManager.isEditorUIElement = function (elm) {
      // Needs to be converted to string since svg can have focus: #6776
      return elm.className.toString().indexOf('mce-') !== -1;
    };

    FocusManager._isUIElement = isUIElement;

    return FocusManager;
  }
);

/**
 * LegacyInput.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
 */

/**
 * Converts legacy input to modern HTML.
 *
 * @class tinymce.LegacyInput
 * @private
 */
define(
  'tinymce.core.LegacyInput',
  [
    "tinymce.core.util.Tools"
  ],
  function (Tools) {
    var each = Tools.each, explode = Tools.explode;

    var register = function (EditorManager) {
      EditorManager.on('AddEditor', function (e) {
        var editor = e.editor;

        editor.on('preInit', function () {
          var filters, fontSizes, dom, settings = editor.settings;

          var replaceWithSpan = function (node, styles) {
            each(styles, function (value, name) {
              if (value) {
                dom.setStyle(node, name, value);
              }
            });

            dom.rename(node, 'span');
          };

          var convert = function (e) {
            dom = editor.dom;

            if (settings.convert_fonts_to_spans) {
              each(dom.select('font,u,strike', e.node), function (node) {
                filters[node.nodeName.toLowerCase()](dom, node);
              });
            }
          };

          if (settings.inline_styles) {
            fontSizes = explode(settings.font_size_legacy_values);

            filters = {
              font: function (dom, node) {
                replaceWithSpan(node, {
                  backgroundColor: node.style.backgroundColor,
                  color: node.color,
                  fontFamily: node.face,
                  fontSize: fontSizes[parseInt(node.size, 10) - 1]
                });
              },

              u: function (dom, node) {
                // HTML5 allows U element
                if (editor.settings.schema === "html4") {
                  replaceWithSpan(node, {
                    textDecoration: 'underline'
                  });
                }
              },

              strike: function (dom, node) {
                replaceWithSpan(node, {
                  textDecoration: 'line-through'
                });
              }
            };

            editor.on('PreProcess SetContent', convert);
          }
        });
      });
    };

    return {
      register: register
    };
  }
);
/**
 * I18n.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
 */

/**
 * I18n class that handles translation of TinyMCE UI.
 * Uses po style with csharp style parameters.
 *
 * @class tinymce.util.I18n
 */
define(
  'tinymce.core.util.I18n',
  [
    "tinymce.core.util.Tools"
  ],
  function (Tools) {
    "use strict";

    var data = {}, code = "en";

    return {
      /**
       * Sets the current language code.
       *
       * @method setCode
       * @param {String} newCode Current language code.
       */
      setCode: function (newCode) {
        if (newCode) {
          code = newCode;
          this.rtl = this.data[newCode] ? this.data[newCode]._dir === 'rtl' : false;
        }
      },

      /**
       * Returns the current language code.
       *
       * @method getCode
       * @return {String} Current language code.
       */
      getCode: function () {
        return code;
      },

      /**
       * Property gets set to true if a RTL language pack was loaded.
       *
       * @property rtl
       * @type Boolean
       */
      rtl: false,

      /**
       * Adds translations for a specific language code.
       *
       * @method add
       * @param {String} code Language code like sv_SE.
       * @param {Array} items Name/value array with English en_US to sv_SE.
       */
      add: function (code, items) {
        var langData = data[code];

        if (!langData) {
          data[code] = langData = {};
        }

        for (var name in items) {
          langData[name] = items[name];
        }

        this.setCode(code);
      },

      /**
       * Translates the specified text.
       *
       * It has a few formats:
       * I18n.translate("Text");
       * I18n.translate(["Text {0}/{1}", 0, 1]);
       * I18n.translate({raw: "Raw string"});
       *
       * @method translate
       * @param {String/Object/Array} text Text to translate.
       * @return {String} String that got translated.
       */
      translate: function (text) {
        var langData = data[code] || {};

        /**
         * number - string
         * null, undefined and empty string - empty string
         * array - comma-delimited string
         * object - in [object Object]
         * function - in [object Function]
         *
         * @param obj
         * @returns {string}
         */
        var toString = function (obj) {
          if (Tools.is(obj, 'function')) {
            return Object.prototype.toString.call(obj);
          }
          return !isEmpty(obj) ? '' + obj : '';
        };

        var isEmpty = function (text) {
          return text === '' || text === null || Tools.is(text, 'undefined');
        };

        var getLangData = function (text) {
          // make sure we work on a string and return a string
          text = toString(text);
          return Tools.hasOwn(langData, text) ? toString(langData[text]) : text;
        };


        if (isEmpty(text)) {
          return '';
        }

        if (Tools.is(text, 'object') && Tools.hasOwn(text, 'raw')) {
          return toString(text.raw);
        }

        if (Tools.is(text, 'array')) {
          var values = text.slice(1);
          text = getLangData(text[0]).replace(/\{([0-9]+)\}/g, function ($1, $2) {
            return Tools.hasOwn(values, $2) ? toString(values[$2]) : $1;
          });
        }

        return getLangData(text).replace(/{context:\w+}$/, '');
      },

      data: data
    };
  }
);
/**
 * EditorManager.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
 */

/**
 * This class used as a factory for manager for tinymce.Editor instances.
 *
 * @example
 * tinymce.EditorManager.init({});
 *
 * @class tinymce.EditorManager
 * @mixes tinymce.util.Observable
 * @static
 */
define(
  'tinymce.core.EditorManager',
  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Type',
    'global!document',
    'global!window',
    'tinymce.core.AddOnManager',
    'tinymce.core.dom.DomQuery',
    'tinymce.core.dom.DOMUtils',
    'tinymce.core.Editor',
    'tinymce.core.Env',
    'tinymce.core.ErrorReporter',
    'tinymce.core.FocusManager',
    'tinymce.core.LegacyInput',
    'tinymce.core.util.I18n',
    'tinymce.core.util.Observable',
    'tinymce.core.util.Promise',
    'tinymce.core.util.Tools',
    'tinymce.core.util.URI'
  ],
  function (Arr, Type, document, window, AddOnManager, DomQuery, DOMUtils, Editor, Env, ErrorReporter, FocusManager, LegacyInput, I18n, Observable, Promise, Tools, URI) {
    var DOM = DOMUtils.DOM;
    var explode = Tools.explode, each = Tools.each, extend = Tools.extend;
    var instanceCounter = 0, beforeUnloadDelegate, EditorManager, boundGlobalEvents = false;
    var legacyEditors = [], editors = [];

    var isValidLegacyKey = function (id) {
      // In theory we could filter out any editor id:s that clash
      // with array prototype items but that could break existing integrations
      return id !== 'length';
    };

    var globalEventDelegate = function (e) {
      each(EditorManager.get(), function (editor) {
        if (e.type === 'scroll') {
          editor.fire('ScrollWindow', e);
        } else {
          editor.fire('ResizeWindow', e);
        }
      });
    };

    var toggleGlobalEvents = function (state) {
      if (state !== boundGlobalEvents) {
        if (state) {
          DomQuery(window).on('resize scroll', globalEventDelegate);
        } else {
          DomQuery(window).off('resize scroll', globalEventDelegate);
        }

        boundGlobalEvents = state;
      }
    };

    var removeEditorFromList = function (targetEditor) {
      var oldEditors = editors;

      delete legacyEditors[targetEditor.id];
      for (var i = 0; i < legacyEditors.length; i++) {
        if (legacyEditors[i] === targetEditor) {
          legacyEditors.splice(i, 1);
          break;
        }
      }

      editors = Arr.filter(editors, function (editor) {
        return targetEditor !== editor;
      });

      // Select another editor since the active one was removed
      if (EditorManager.activeEditor === targetEditor) {
        EditorManager.activeEditor = editors.length > 0 ? editors[0] : null;
      }

      // Clear focusedEditor if necessary, so that we don't try to blur the destroyed editor
      if (EditorManager.focusedEditor === targetEditor) {
        EditorManager.focusedEditor = null;
      }

      return oldEditors.length !== editors.length;
    };

    var purgeDestroyedEditor = function (editor) {
      // User has manually destroyed the editor lets clean up the mess
      if (editor && editor.initialized && !(editor.getContainer() || editor.getBody()).parentNode) {
        removeEditorFromList(editor);
        editor.unbindAllNativeEvents();
        editor.destroy(true);
        editor.removed = true;
        editor = null;
      }

      return editor;
    };

    EditorManager = {
      defaultSettings: {},

      /**
       * Dom query instance.
       *
       * @property $
       * @type tinymce.dom.DomQuery
       */
      $: DomQuery,

      /**
       * Major version of TinyMCE build.
       *
       * @property majorVersion
       * @type String
       */
      majorVersion: '4',

      /**
       * Minor version of TinyMCE build.
       *
       * @property minorVersion
       * @type String
       */
      minorVersion: '7.1',

      /**
       * Release date of TinyMCE build.
       *
       * @property releaseDate
       * @type String
       */
      releaseDate: '2017-10-09',

      /**
       * Collection of editor instances. Deprecated use tinymce.get() instead.
       *
       * @property editors
       * @type Object
       */
      editors: legacyEditors,

      /**
       * Collection of language pack data.
       *
       * @property i18n
       * @type Object
       */
      i18n: I18n,

      /**
       * Currently active editor instance.
       *
       * @property activeEditor
       * @type tinymce.Editor
       * @example
       * tinyMCE.activeEditor.selection.getContent();
       * tinymce.EditorManager.activeEditor.selection.getContent();
       */
      activeEditor: null,

      settings: {},

      setup: function () {
        var self = this, baseURL, documentBaseURL, suffix = "", preInit, src;

        // Get base URL for the current document
        documentBaseURL = URI.getDocumentBaseUrl(document.location);

        // Check if the URL is a document based format like: http://site/dir/file and file:///
        // leave other formats like applewebdata://... intact
        if (/^[^:]+:\/\/\/?[^\/]+\//.test(documentBaseURL)) {
          documentBaseURL = documentBaseURL.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');

          if (!/[\/\\]$/.test(documentBaseURL)) {
            documentBaseURL += '/';
          }
        }

        // If tinymce is defined and has a base use that or use the old tinyMCEPreInit
        preInit = window.tinymce || window.tinyMCEPreInit;
        if (preInit) {
          baseURL = preInit.base || preInit.baseURL;
          suffix = preInit.suffix;
        } else {
          // Get base where the tinymce script is located
          var scripts = document.getElementsByTagName('script');
          for (var i = 0; i < scripts.length; i++) {
            src = scripts[i].src;

            // Script types supported:
            // tinymce.js tinymce.min.js tinymce.dev.js
            // tinymce.jquery.js tinymce.jquery.min.js tinymce.jquery.dev.js
            // tinymce.full.js tinymce.full.min.js tinymce.full.dev.js
            var srcScript = src.substring(src.lastIndexOf('/'));
            if (/tinymce(\.full|\.jquery|)(\.min|\.dev|)\.js/.test(src)) {
              if (srcScript.indexOf('.min') != -1) {
                suffix = '.min';
              }

              baseURL = src.substring(0, src.lastIndexOf('/'));
              break;
            }
          }

          // We didn't find any baseURL by looking at the script elements
          // Try to use the document.currentScript as a fallback
          if (!baseURL && document.currentScript) {
            src = document.currentScript.src;

            if (src.indexOf('.min') != -1) {
              suffix = '.min';
            }

            baseURL = src.substring(0, src.lastIndexOf('/'));
          }
        }

        /**
         * Base URL where the root directory if TinyMCE is located.
         *
         * @property baseURL
         * @type String
         */
        self.baseURL = new URI(documentBaseURL).toAbsolute(baseURL);

        /**
         * Document base URL where the current document is located.
         *
         * @property documentBaseURL
         * @type String
         */
        self.documentBaseURL = documentBaseURL;

        /**
         * Absolute baseURI for the installation path of TinyMCE.
         *
         * @property baseURI
         * @type tinymce.util.URI
         */
        self.baseURI = new URI(self.baseURL);

        /**
         * Current suffix to add to each plugin/theme that gets loaded for example ".min".
         *
         * @property suffix
         * @type String
         */
        self.suffix = suffix;

        self.focusManager = new FocusManager(self);
      },

      /**
       * Overrides the default settings for editor instances.
       *
       * @method overrideDefaults
       * @param {Object} defaultSettings Defaults settings object.
       */
      overrideDefaults: function (defaultSettings) {
        var baseUrl, suffix;

        baseUrl = defaultSettings.base_url;
        if (baseUrl) {
          this.baseURL = new URI(this.documentBaseURL).toAbsolute(baseUrl.replace(/\/+$/, ''));
          this.baseURI = new URI(this.baseURL);
        }

        suffix = defaultSettings.suffix;
        if (defaultSettings.suffix) {
          this.suffix = suffix;
        }

        this.defaultSettings = defaultSettings;

        var pluginBaseUrls = defaultSettings.plugin_base_urls;
        for (var name in pluginBaseUrls) {
          AddOnManager.PluginManager.urls[name] = pluginBaseUrls[name];
        }
      },

      /**
       * Initializes a set of editors. This method will create editors based on various settings.
       *
       * @method init
       * @param {Object} settings Settings object to be passed to each editor instance.
       * @return {tinymce.util.Promise} Promise that gets resolved with an array of editors when all editor instances are initialized.
       * @example
       * // Initializes a editor using the longer method
       * tinymce.EditorManager.init({
       *    some_settings : 'some value'
       * });
       *
       * // Initializes a editor instance using the shorter version and with a promise
       * tinymce.init({
       *    some_settings : 'some value'
       * }).then(function(editors) {
       *    ...
       * });
       */
      init: function (settings) {
        var self = this, result, invalidInlineTargets;

        invalidInlineTargets = Tools.makeMap(
          'area base basefont br col frame hr img input isindex link meta param embed source wbr track ' +
          'colgroup option tbody tfoot thead tr script noscript style textarea video audio iframe object menu',
          ' '
        );

        var isInvalidInlineTarget = function (settings, elm) {
          return settings.inline && elm.tagName.toLowerCase() in invalidInlineTargets;
        };

        var createId = function (elm) {
          var id = elm.id;

          // Use element id, or unique name or generate a unique id
          if (!id) {
            id = elm.name;

            if (id && !DOM.get(id)) {
              id = elm.name;
            } else {
              // Generate unique name
              id = DOM.uniqueId();
            }

            elm.setAttribute('id', id);
          }

          return id;
        };

        var execCallback = function (name) {
          var callback = settings[name];

          if (!callback) {
            return;
          }

          return callback.apply(self, Array.prototype.slice.call(arguments, 2));
        };

        var hasClass = function (elm, className) {
          return className.constructor === RegExp ? className.test(elm.className) : DOM.hasClass(elm, className);
        };

        var findTargets = function (settings) {
          var l, targets = [];

          if (Env.ie && Env.ie < 11) {
            ErrorReporter.initError(
              'TinyMCE does not support the browser you are using. For a list of supported' +
              ' browsers please see: https://www.tinymce.com/docs/get-started/system-requirements/'
            );
            return [];
          }

          if (settings.types) {
            each(settings.types, function (type) {
              targets = targets.concat(DOM.select(type.selector));
            });

            return targets;
          } else if (settings.selector) {
            return DOM.select(settings.selector);
          } else if (settings.target) {
            return [settings.target];
          }

          // Fallback to old setting
          switch (settings.mode) {
            case "exact":
              l = settings.elements || '';

              if (l.length > 0) {
                each(explode(l), function (id) {
                  var elm;

                  if ((elm = DOM.get(id))) {
                    targets.push(elm);
                  } else {
                    each(document.forms, function (f) {
                      each(f.elements, function (e) {
                        if (e.name === id) {
                          id = 'mce_editor_' + instanceCounter++;
                          DOM.setAttrib(e, 'id', id);
                          targets.push(e);
                        }
                      });
                    });
                  }
                });
              }
              break;

            case "textareas":
            case "specific_textareas":
              each(DOM.select('textarea'), function (elm) {
                if (settings.editor_deselector && hasClass(elm, settings.editor_deselector)) {
                  return;
                }

                if (!settings.editor_selector || hasClass(elm, settings.editor_selector)) {
                  targets.push(elm);
                }
              });
              break;
          }

          return targets;
        };

        var provideResults = function (editors) {
          result = editors;
        };

        var initEditors = function () {
          var initCount = 0, editors = [], targets;

          var createEditor = function (id, settings, targetElm) {
            var editor = new Editor(id, settings, self);

            editors.push(editor);

            editor.on('init', function () {
              if (++initCount === targets.length) {
                provideResults(editors);
              }
            });

            editor.targetElm = editor.targetElm || targetElm;
            editor.render();
          };

          DOM.unbind(window, 'ready', initEditors);
          execCallback('onpageload');

          targets = DomQuery.unique(findTargets(settings));

          // TODO: Deprecate this one
          if (settings.types) {
            each(settings.types, function (type) {
              Tools.each(targets, function (elm) {
                if (DOM.is(elm, type.selector)) {
                  createEditor(createId(elm), extend({}, settings, type), elm);
                  return false;
                }

                return true;
              });
            });

            return;
          }

          Tools.each(targets, function (elm) {
            purgeDestroyedEditor(self.get(elm.id));
          });

          targets = Tools.grep(targets, function (elm) {
            return !self.get(elm.id);
          });

          if (targets.length === 0) {
            provideResults([]);
          } else {
            each(targets, function (elm) {
              if (isInvalidInlineTarget(settings, elm)) {
                ErrorReporter.initError('Could not initialize inline editor on invalid inline target element', elm);
              } else {
                createEditor(createId(elm), settings, elm);
              }
            });
          }
        };

        self.settings = settings;
        DOM.bind(window, 'ready', initEditors);

        return new Promise(function (resolve) {
          if (result) {
            resolve(result);
          } else {
            provideResults = function (editors) {
              resolve(editors);
            };
          }
        });
      },

      /**
       * Returns a editor instance by id.
       *
       * @method get
       * @param {String/Number} id Editor instance id or index to return.
       * @return {tinymce.Editor/Array} Editor instance to return or array of editor instances.
       * @example
       * // Adds an onclick event to an editor by id
       * tinymce.get('mytextbox').on('click', function(e) {
       *    ed.windowManager.alert('Hello world!');
       * });
       *
       * // Adds an onclick event to an editor by index
       * tinymce.get(0).on('click', function(e) {
       *    ed.windowManager.alert('Hello world!');
       * });
       *
       * // Adds an onclick event to an editor by id (longer version)
       * tinymce.EditorManager.get('mytextbox').on('click', function(e) {
       *    ed.windowManager.alert('Hello world!');
       * });
       */
      get: function (id) {
        if (arguments.length === 0) {
          return editors.slice(0);
        } else if (Type.isString(id)) {
          return Arr.find(editors, function (editor) {
            return editor.id === id;
          }).getOr(null);
        } else if (Type.isNumber(id)) {
          return editors[id] ? editors[id] : null;
        } else {
          return null;
        }
      },

      /**
       * Adds an editor instance to the editor collection. This will also set it as the active editor.
       *
       * @method add
       * @param {tinymce.Editor} editor Editor instance to add to the collection.
       * @return {tinymce.Editor} The same instance that got passed in.
       */
      add: function (editor) {
        var self = this, existingEditor;

        // Prevent existing editors from beeing added again this could happen
        // if a user calls createEditor then render or add multiple times.
        existingEditor = legacyEditors[editor.id];
        if (existingEditor === editor) {
          return editor;
        }

        if (self.get(editor.id) === null) {
          // Add to legacy editors array, this is what breaks in HTML5 where ID:s with numbers are valid
          // We can't get rid of this strange object and array at the same time since it seems to be used all over the web
          if (isValidLegacyKey(editor.id)) {
            legacyEditors[editor.id] = editor;
          }

          legacyEditors.push(editor);

          editors.push(editor);
        }

        toggleGlobalEvents(true);

        // Doesn't call setActive method since we don't want
        // to fire a bunch of activate/deactivate calls while initializing
        self.activeEditor = editor;

        self.fire('AddEditor', { editor: editor });

        if (!beforeUnloadDelegate) {
          beforeUnloadDelegate = function () {
            self.fire('BeforeUnload');
          };

          DOM.bind(window, 'beforeunload', beforeUnloadDelegate);
        }

        return editor;
      },

      /**
       * Creates an editor instance and adds it to the EditorManager collection.
       *
       * @method createEditor
       * @param {String} id Instance id to use for editor.
       * @param {Object} settings Editor instance settings.
       * @return {tinymce.Editor} Editor instance that got created.
       */
      createEditor: function (id, settings) {
        return this.add(new Editor(id, settings, this));
      },

      /**
       * Removes a editor or editors form page.
       *
       * @example
       * // Remove all editors bound to divs
       * tinymce.remove('div');
       *
       * // Remove all editors bound to textareas
       * tinymce.remove('textarea');
       *
       * // Remove all editors
       * tinymce.remove();
       *
       * // Remove specific instance by id
       * tinymce.remove('#id');
       *
       * @method remove
       * @param {tinymce.Editor/String/Object} [selector] CSS selector or editor instance to remove.
       * @return {tinymce.Editor} The editor that got passed in will be return if it was found otherwise null.
       */
      remove: function (selector) {
        var self = this, i, editor;

        // Remove all editors
        if (!selector) {
          for (i = editors.length - 1; i >= 0; i--) {
            self.remove(editors[i]);
          }

          return;
        }

        // Remove editors by selector
        if (Type.isString(selector)) {
          selector = selector.selector || selector;

          each(DOM.select(selector), function (elm) {
            editor = self.get(elm.id);

            if (editor) {
              self.remove(editor);
            }
          });

          return;
        }

        // Remove specific editor
        editor = selector;

        // Not in the collection
        if (Type.isNull(self.get(editor.id))) {
          return null;
        }

        if (removeEditorFromList(editor)) {
          self.fire('RemoveEditor', { editor: editor });
        }

        if (editors.length === 0) {
          DOM.unbind(window, 'beforeunload', beforeUnloadDelegate);
        }

        editor.remove();

        toggleGlobalEvents(editors.length > 0);

        return editor;
      },

      /**
       * Executes a specific command on the currently active editor.
       *
       * @method execCommand
       * @param {String} cmd Command to perform for example Bold.
       * @param {Boolean} ui Optional boolean state if a UI should be presented for the command or not.
       * @param {String} value Optional value parameter like for example an URL to a link.
       * @return {Boolean} true/false if the command was executed or not.
       */
      execCommand: function (cmd, ui, value) {
        var self = this, editor = self.get(value);

        // Manager commands
        switch (cmd) {
          case "mceAddEditor":
            if (!self.get(value)) {
              new Editor(value, self.settings, self).render();
            }

            return true;

          case "mceRemoveEditor":
            if (editor) {
              editor.remove();
            }

            return true;

          case 'mceToggleEditor':
            if (!editor) {
              self.execCommand('mceAddEditor', 0, value);
              return true;
            }

            if (editor.isHidden()) {
              editor.show();
            } else {
              editor.hide();
            }

            return true;
        }

        // Run command on active editor
        if (self.activeEditor) {
          return self.activeEditor.execCommand(cmd, ui, value);
        }

        return false;
      },

      /**
       * Calls the save method on all editor instances in the collection. This can be useful when a form is to be submitted.
       *
       * @method triggerSave
       * @example
       * // Saves all contents
       * tinyMCE.triggerSave();
       */
      triggerSave: function () {
        each(editors, function (editor) {
          editor.save();
        });
      },

      /**
       * Adds a language pack, this gets called by the loaded language files like en.js.
       *
       * @method addI18n
       * @param {String} code Optional language code.
       * @param {Object} items Name/value object with translations.
       */
      addI18n: function (code, items) {
        I18n.add(code, items);
      },

      /**
       * Translates the specified string using the language pack items.
       *
       * @method translate
       * @param {String/Array/Object} text String to translate
       * @return {String} Translated string.
       */
      translate: function (text) {
        return I18n.translate(text);
      },

      /**
       * Sets the active editor instance and fires the deactivate/activate events.
       *
       * @method setActive
       * @param {tinymce.Editor} editor Editor instance to set as the active instance.
       */
      setActive: function (editor) {
        var activeEditor = this.activeEditor;

        if (this.activeEditor != editor) {
          if (activeEditor) {
            activeEditor.fire('deactivate', { relatedTarget: editor });
          }

          editor.fire('activate', { relatedTarget: activeEditor });
        }

        this.activeEditor = editor;
      }
    };

    extend(EditorManager, Observable);

    EditorManager.setup();
    LegacyInput.register(EditorManager);

    return EditorManager;
  }
);

/**
 * Rect.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
 */

/**
 * Contains various tools for rect/position calculation.
 *
 * @class tinymce.geom.Rect
 */
define(
  'tinymce.core.geom.Rect',
  [
  ],
  function () {
    "use strict";

    var min = Math.min, max = Math.max, round = Math.round;

    /**
     * Returns the rect positioned based on the relative position name
     * to the target rect.
     *
     * @method relativePosition
     * @param {Rect} rect Source rect to modify into a new rect.
     * @param {Rect} targetRect Rect to move relative to based on the rel option.
     * @param {String} rel Relative position. For example: tr-bl.
     */
    var relativePosition = function (rect, targetRect, rel) {
      var x, y, w, h, targetW, targetH;

      x = targetRect.x;
      y = targetRect.y;
      w = rect.w;
      h = rect.h;
      targetW = targetRect.w;
      targetH = targetRect.h;

      rel = (rel || '').split('');

      if (rel[0] === 'b') {
        y += targetH;
      }

      if (rel[1] === 'r') {
        x += targetW;
      }

      if (rel[0] === 'c') {
        y += round(targetH / 2);
      }

      if (rel[1] === 'c') {
        x += round(targetW / 2);
      }

      if (rel[3] === 'b') {
        y -= h;
      }

      if (rel[4] === 'r') {
        x -= w;
      }

      if (rel[3] === 'c') {
        y -= round(h / 2);
      }

      if (rel[4] === 'c') {
        x -= round(w / 2);
      }

      return create(x, y, w, h);
    };

    /**
     * Tests various positions to get the most suitable one.
     *
     * @method findBestRelativePosition
     * @param {Rect} rect Rect to use as source.
     * @param {Rect} targetRect Rect to move relative to.
     * @param {Rect} constrainRect Rect to constrain within.
     * @param {Array} rels Array of relative positions to test against.
     */
    var findBestRelativePosition = function (rect, targetRect, constrainRect, rels) {
      var pos, i;

      for (i = 0; i < rels.length; i++) {
        pos = relativePosition(rect, targetRect, rels[i]);

        if (pos.x >= constrainRect.x && pos.x + pos.w <= constrainRect.w + constrainRect.x &&
          pos.y >= constrainRect.y && pos.y + pos.h <= constrainRect.h + constrainRect.y) {
          return rels[i];
        }
      }

      return null;
    };

    /**
     * Inflates the rect in all directions.
     *
     * @method inflate
     * @param {Rect} rect Rect to expand.
     * @param {Number} w Relative width to expand by.
     * @param {Number} h Relative height to expand by.
     * @return {Rect} New expanded rect.
     */
    var inflate = function (rect, w, h) {
      return create(rect.x - w, rect.y - h, rect.w + w * 2, rect.h + h * 2);
    };

    /**
     * Returns the intersection of the specified rectangles.
     *
     * @method intersect
     * @param {Rect} rect The first rectangle to compare.
     * @param {Rect} cropRect The second rectangle to compare.
     * @return {Rect} The intersection of the two rectangles or null if they don't intersect.
     */
    var intersect = function (rect, cropRect) {
      var x1, y1, x2, y2;

      x1 = max(rect.x, cropRect.x);
      y1 = max(rect.y, cropRect.y);
      x2 = min(rect.x + rect.w, cropRect.x + cropRect.w);
      y2 = min(rect.y + rect.h, cropRect.y + cropRect.h);

      if (x2 - x1 < 0 || y2 - y1 < 0) {
        return null;
      }

      return create(x1, y1, x2 - x1, y2 - y1);
    };

    /**
     * Returns a rect clamped within the specified clamp rect. This forces the
     * rect to be inside the clamp rect.
     *
     * @method clamp
     * @param {Rect} rect Rectangle to force within clamp rect.
     * @param {Rect} clampRect Rectable to force within.
     * @param {Boolean} fixedSize True/false if size should be fixed.
     * @return {Rect} Clamped rect.
     */
    var clamp = function (rect, clampRect, fixedSize) {
      var underflowX1, underflowY1, overflowX2, overflowY2,
        x1, y1, x2, y2, cx2, cy2;

      x1 = rect.x;
      y1 = rect.y;
      x2 = rect.x + rect.w;
      y2 = rect.y + rect.h;
      cx2 = clampRect.x + clampRect.w;
      cy2 = clampRect.y + clampRect.h;

      underflowX1 = max(0, clampRect.x - x1);
      underflowY1 = max(0, clampRect.y - y1);
      overflowX2 = max(0, x2 - cx2);
      overflowY2 = max(0, y2 - cy2);

      x1 += underflowX1;
      y1 += underflowY1;

      if (fixedSize) {
        x2 += underflowX1;
        y2 += underflowY1;
        x1 -= overflowX2;
        y1 -= overflowY2;
      }

      x2 -= overflowX2;
      y2 -= overflowY2;

      return create(x1, y1, x2 - x1, y2 - y1);
    };

    /**
     * Creates a new rectangle object.
     *
     * @method create
     * @param {Number} x Rectangle x location.
     * @param {Number} y Rectangle y location.
     * @param {Number} w Rectangle width.
     * @param {Number} h Rectangle height.
     * @return {Rect} New rectangle object.
     */
    var create = function (x, y, w, h) {
      return { x: x, y: y, w: w, h: h };
    };

    /**
     * Creates a new rectangle object form a clientRects object.
     *
     * @method fromClientRect
     * @param {ClientRect} clientRect DOM ClientRect object.
     * @return {Rect} New rectangle object.
     */
    var fromClientRect = function (clientRect) {
      return create(clientRect.left, clientRect.top, clientRect.width, clientRect.height);
    };

    return {
      inflate: inflate,
      relativePosition: relativePosition,
      findBestRelativePosition: findBestRelativePosition,
      intersect: intersect,
      clamp: clamp,
      create: create,
      fromClientRect: fromClientRect
    };
  }
);

/**
 * Factory.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
 */

/**
 * This class is a factory for control instances. This enables you
 * to create instances of controls without having to require the UI controls directly.
 *
 * It also allow you to override or add new control types.
 *
 * @class tinymce.ui.Factory
 */
define(
  'tinymce.core.ui.Factory',
  [
  ],
  function () {
    "use strict";

    var types = {};

    return {
      /**
       * Adds a new control instance type to the factory.
       *
       * @method add
       * @param {String} type Type name for example "button".
       * @param {function} typeClass Class type function.
       */
      add: function (type, typeClass) {
        types[type.toLowerCase()] = typeClass;
      },

      /**
       * Returns true/false if the specified type exists or not.
       *
       * @method has
       * @param {String} type Type to look for.
       * @return {Boolean} true/false if the control by name exists.
       */
      has: function (type) {
        return !!types[type.toLowerCase()];
      },

      /**
       * Returns ui control module by name.
       *
       * @method get
       * @param {String} type Type get.
       * @return {Object} Module or undefined.
       */
      get: function (type) {
        var lctype = type.toLowerCase();
        var controlType = types.hasOwnProperty(lctype) ? types[lctype] : null;
        if (controlType === null) {
          throw new Error("Could not find module for type: " + type);
        }

        return controlType;
      },

      /**
       * Creates a new control instance based on the settings provided. The instance created will be
       * based on the specified type property it can also create whole structures of components out of
       * the specified JSON object.
       *
       * @example
       * tinymce.ui.Factory.create({
       *     type: 'button',
       *     text: 'Hello world!'
       * });
       *
       * @method create
       * @param {Object/String} settings Name/Value object with items used to create the type.
       * @return {tinymce.ui.Control} Control instance based on the specified type.
       */
      create: function (type, settings) {
        var ControlType;

        // If string is specified then use it as the type
        if (typeof type == 'string') {
          settings = settings || {};
          settings.type = type;
        } else {
          settings = type;
          type = settings.type;
        }

        // Find control type
        type = type.toLowerCase();
        ControlType = types[type];

        // #if debug

        if (!ControlType) {
          throw new Error("Could not find control by type: " + type);
        }

        // #endif

        ControlType = new ControlType(settings);
        ControlType.type = type; // Set the type on the instance, this will be used by the Selector engine

        return ControlType;
      }
    };
  }
);
/**
 * Class.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
 */

/**
 * This utilitiy class is used for easier inheritance.
 *
 * Features:
 * * Exposed super functions: this._super();
 * * Mixins
 * * Dummy functions
 * * Property functions: var value = object.value(); and object.value(newValue);
 * * Static functions
 * * Defaults settings
 */
define(
  'tinymce.core.util.Class',
  [
    "tinymce.core.util.Tools"
  ],
  function (Tools) {
    var each = Tools.each, extend = Tools.extend;

    var extendClass, initializing;

    var Class = function () {
    };

    // Provides classical inheritance, based on code made by John Resig
    Class.extend = extendClass = function (prop) {
      var self = this, _super = self.prototype, prototype, name, member;

      // The dummy class constructor
      var Class = function () {
        var i, mixins, mixin, self = this;

        // All construction is actually done in the init method
        if (!initializing) {
          // Run class constuctor
          if (self.init) {
            self.init.apply(self, arguments);
          }

          // Run mixin constructors
          mixins = self.Mixins;
          if (mixins) {
            i = mixins.length;
            while (i--) {
              mixin = mixins[i];
              if (mixin.init) {
                mixin.init.apply(self, arguments);
              }
            }
          }
        }
      };

      // Dummy function, needs to be extended in order to provide functionality
      var dummy = function () {
        return this;
      };

      // Creates a overloaded method for the class
      // this enables you to use this._super(); to call the super function
      var createMethod = function (name, fn) {
        return function () {
          var self = this, tmp = self._super, ret;

          self._super = _super[name];
          ret = fn.apply(self, arguments);
          self._super = tmp;

          return ret;
        };
      };

      // Instantiate a base class (but only create the instance,
      // don't run the init constructor)
      initializing = true;

      /*eslint new-cap:0 */
      prototype = new self();
      initializing = false;

      // Add mixins
      if (prop.Mixins) {
        each(prop.Mixins, function (mixin) {
          for (var name in mixin) {
            if (name !== "init") {
              prop[name] = mixin[name];
            }
          }
        });

        if (_super.Mixins) {
          prop.Mixins = _super.Mixins.concat(prop.Mixins);
        }
      }

      // Generate dummy methods
      if (prop.Methods) {
        each(prop.Methods.split(','), function (name) {
          prop[name] = dummy;
        });
      }

      // Generate property methods
      if (prop.Properties) {
        each(prop.Properties.split(','), function (name) {
          var fieldName = '_' + name;

          prop[name] = function (value) {
            var self = this, undef;

            // Set value
            if (value !== undef) {
              self[fieldName] = value;

              return self;
            }

            // Get value
            return self[fieldName];
          };
        });
      }

      // Static functions
      if (prop.Statics) {
        each(prop.Statics, function (func, name) {
          Class[name] = func;
        });
      }

      // Default settings
      if (prop.Defaults && _super.Defaults) {
        prop.Defaults = extend({}, _super.Defaults, prop.Defaults);
      }

      // Copy the properties over onto the new prototype
      for (name in prop) {
        member = prop[name];

        if (typeof member == "function" && _super[name]) {
          prototype[name] = createMethod(name, member);
        } else {
          prototype[name] = member;
        }
      }

      // Populate our constructed prototype object
      Class.prototype = prototype;

      // Enforce the constructor to be what we expect
      Class.constructor = Class;

      // And make this class extendible
      Class.extend = extendClass;

      return Class;
    };

    return Class;
  }
);
/**
 * Color.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
 */

/**
 * This class lets you parse/serialize colors and convert rgb/hsb.
 *
 * @class tinymce.util.Color
 * @example
 * var white = new tinymce.util.Color({r: 255, g: 255, b: 255});
 * var red = new tinymce.util.Color('#FF0000');
 *
 * console.log(white.toHex(), red.toHsv());
 */
define(
  'tinymce.core.util.Color',
  [
  ],
  function () {
    var min = Math.min, max = Math.max, round = Math.round;

    /**
     * Constructs a new color instance.
     *
     * @constructor
     * @method Color
     * @param {String} value Optional initial value to parse.
     */
    var Color = function (value) {
      var self = this, r = 0, g = 0, b = 0;

      var rgb2hsv = function (r, g, b) {
        var h, s, v, d, minRGB, maxRGB;

        h = 0;
        s = 0;
        v = 0;
        r = r / 255;
        g = g / 255;
        b = b / 255;

        minRGB = min(r, min(g, b));
        maxRGB = max(r, max(g, b));

        if (minRGB == maxRGB) {
          v = minRGB;

          return {
            h: 0,
            s: 0,
            v: v * 100
          };
        }

        /*eslint no-nested-ternary:0 */
        d = (r == minRGB) ? g - b : ((b == minRGB) ? r - g : b - r);
        h = (r == minRGB) ? 3 : ((b == minRGB) ? 1 : 5);
        h = 60 * (h - d / (maxRGB - minRGB));
        s = (maxRGB - minRGB) / maxRGB;
        v = maxRGB;

        return {
          h: round(h),
          s: round(s * 100),
          v: round(v * 100)
        };
      };

      var hsvToRgb = function (hue, saturation, brightness) {
        var side, chroma, x, match;

        hue = (parseInt(hue, 10) || 0) % 360;
        saturation = parseInt(saturation, 10) / 100;
        brightness = parseInt(brightness, 10) / 100;
        saturation = max(0, min(saturation, 1));
        brightness = max(0, min(brightness, 1));

        if (saturation === 0) {
          r = g = b = round(255 * brightness);
          return;
        }

        side = hue / 60;
        chroma = brightness * saturation;
        x = chroma * (1 - Math.abs(side % 2 - 1));
        match = brightness - chroma;

        switch (Math.floor(side)) {
          case 0:
            r = chroma;
            g = x;
            b = 0;
            break;

          case 1:
            r = x;
            g = chroma;
            b = 0;
            break;

          case 2:
            r = 0;
            g = chroma;
            b = x;
            break;

          case 3:
            r = 0;
            g = x;
            b = chroma;
            break;

          case 4:
            r = x;
            g = 0;
            b = chroma;
            break;

          case 5:
            r = chroma;
            g = 0;
            b = x;
            break;

          default:
            r = g = b = 0;
        }

        r = round(255 * (r + match));
        g = round(255 * (g + match));
        b = round(255 * (b + match));
      };

      /**
       * Returns the hex string of the current color. For example: #ff00ff
       *
       * @method toHex
       * @return {String} Hex string of current color.
       */
      var toHex = function () {
        var hex = function (val) {
          val = parseInt(val, 10).toString(16);

          return val.length > 1 ? val : '0' + val;
        };

        return '#' + hex(r) + hex(g) + hex(b);
      };

      /**
       * Returns the r, g, b values of the color. Each channel has a range from 0-255.
       *
       * @method toRgb
       * @return {Object} Object with r, g, b fields.
       */
      var toRgb = function () {
        return {
          r: r,
          g: g,
          b: b
        };
      };

      /**
       * Returns the h, s, v values of the color. Ranges: h=0-360, s=0-100, v=0-100.
       *
       * @method toHsv
       * @return {Object} Object with h, s, v fields.
       */
      var toHsv = function () {
        return rgb2hsv(r, g, b);
      };

      /**
       * Parses the specified value and populates the color instance.
       *
       * Supported format examples:
       *  * rbg(255,0,0)
       *  * #ff0000
       *  * #fff
       *  * {r: 255, g: 0, b: 0}
       *  * {h: 360, s: 100, v: 100}
       *
       * @method parse
       * @param {Object/String} value Color value to parse.
       * @return {tinymce.util.Color} Current color instance.
       */
      var parse = function (value) {
        var matches;

        if (typeof value == 'object') {
          if ("r" in value) {
            r = value.r;
            g = value.g;
            b = value.b;
          } else if ("v" in value) {
            hsvToRgb(value.h, value.s, value.v);
          }
        } else {
          if ((matches = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)[^\)]*\)/gi.exec(value))) {
            r = parseInt(matches[1], 10);
            g = parseInt(matches[2], 10);
            b = parseInt(matches[3], 10);
          } else if ((matches = /#([0-F]{2})([0-F]{2})([0-F]{2})/gi.exec(value))) {
            r = parseInt(matches[1], 16);
            g = parseInt(matches[2], 16);
            b = parseInt(matches[3], 16);
          } else if ((matches = /#([0-F])([0-F])([0-F])/gi.exec(value))) {
            r = parseInt(matches[1] + matches[1], 16);
            g = parseInt(matches[2] + matches[2], 16);
            b = parseInt(matches[3] + matches[3], 16);
          }
        }

        r = r < 0 ? 0 : (r > 255 ? 255 : r);
        g = g < 0 ? 0 : (g > 255 ? 255 : g);
        b = b < 0 ? 0 : (b > 255 ? 255 : b);

        return self;
      };

      if (value) {
        parse(value);
      }

      self.toRgb = toRgb;
      self.toHsv = toHsv;
      self.toHex = toHex;
      self.parse = parse;
    };

    return Color;
  }
);

/**
 * JSON.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
 */

/**
 * JSON parser and serializer class.
 *
 * @class tinymce.util.JSON
 * @static
 * @example
 * // JSON parse a string into an object
 * var obj = tinymce.util.JSON.parse(somestring);
 *
 * // JSON serialize a object into an string
 * var str = tinymce.util.JSON.serialize(obj);
 */
define(
  'tinymce.core.util.JSON',
  [
    'global!window'
  ],
  function (window) {
    var serialize = function (o, quote) {
      var i, v, t, name;

      quote = quote || '"';

      if (o === null) {
        return 'null';
      }

      t = typeof o;

      if (t == 'string') {
        v = '\bb\tt\nn\ff\rr\""\'\'\\\\';

        /*eslint no-control-regex:0 */
        return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function (a, b) {
          // Make sure single quotes never get encoded inside double quotes for JSON compatibility
          if (quote === '"' && a === "'") {
            return a;
          }

          i = v.indexOf(b);

          if (i + 1) {
            return '\\' + v.charAt(i + 1);
          }

          a = b.charCodeAt().toString(16);

          return '\\u' + '0000'.substring(a.length) + a;
        }) + quote;
      }

      if (t == 'object') {
        if (o.hasOwnProperty && Object.prototype.toString.call(o) === '[object Array]') {
          for (i = 0, v = '['; i < o.length; i++) {
            v += (i > 0 ? ',' : '') + serialize(o[i], quote);
          }

          return v + ']';
        }

        v = '{';

        for (name in o) {
          if (o.hasOwnProperty(name)) {
            v += typeof o[name] != 'function' ? (v.length > 1 ? ',' + quote : quote) + name +
              quote + ':' + serialize(o[name], quote) : '';
          }
        }

        return v + '}';
      }

      return '' + o;
    };

    return {
      /**
       * Serializes the specified object as a JSON string.
       *
       * @method serialize
       * @param {Object} obj Object to serialize as a JSON string.
       * @param {String} quote Optional quote string defaults to ".
       * @return {string} JSON string serialized from input.
       */
      serialize: serialize,

      /**
       * Unserializes/parses the specified JSON string into a object.
       *
       * @method parse
       * @param {string} s JSON String to parse into a JavaScript object.
       * @return {Object} Object from input JSON string or undefined if it failed.
       */
      parse: function (text) {
        try {
          // Trick uglify JS
          return window[String.fromCharCode(101) + 'val']('(' + text + ')');
        } catch (ex) {
          // Ignore
        }
      }

      /**#@-*/
    };
  }
);

/**
 * JSONP.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
 */

define(
  'tinymce.core.util.JSONP',
  [
    "tinymce.core.dom.DOMUtils"
  ],
  function (DOMUtils) {
    return {
      callbacks: {},
      count: 0,

      send: function (settings) {
        var self = this, dom = DOMUtils.DOM, count = settings.count !== undefined ? settings.count : self.count;
        var id = 'tinymce_jsonp_' + count;

        self.callbacks[count] = function (json) {
          dom.remove(id);
          delete self.callbacks[count];

          settings.callback(json);
        };

        dom.add(dom.doc.body, 'script', {
          id: id,
          src: settings.url,
          type: 'text/javascript'
        });

        self.count++;
      }
    };
  }
);
/**
 * XHR.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
 */

/**
 * This class enables you to send XMLHTTPRequests cross browser.
 * @class tinymce.util.XHR
 * @mixes tinymce.util.Observable
 * @static
 * @example
 * // Sends a low level Ajax request
 * tinymce.util.XHR.send({
 *    url: 'someurl',
 *    success: function(text) {
 *       console.debug(text);
 *    }
 * });
 *
 * // Add custom header to XHR request
 * tinymce.util.XHR.on('beforeSend', function(e) {
 *     e.xhr.setRequestHeader('X-Requested-With', 'Something');
 * });
 */
define(
  'tinymce.core.util.XHR',
  [
    'ephox.sand.api.XMLHttpRequest',
    'global!setTimeout',
    'tinymce.core.util.Observable',
    'tinymce.core.util.Tools'
  ],
  function (XMLHttpRequest, setTimeout, Observable, Tools) {
    var XHR = {
      /**
       * Sends a XMLHTTPRequest.
       * Consult the Wiki for details on what settings this method takes.
       *
       * @method send
       * @param {Object} settings Object will target URL, callbacks and other info needed to make the request.
       */
      send: function (settings) {
        var xhr, count = 0;

        var ready = function () {
          if (!settings.async || xhr.readyState == 4 || count++ > 10000) {
            if (settings.success && count < 10000 && xhr.status == 200) {
              settings.success.call(settings.success_scope, '' + xhr.responseText, xhr, settings);
            } else if (settings.error) {
              settings.error.call(settings.error_scope, count > 10000 ? 'TIMED_OUT' : 'GENERAL', xhr, settings);
            }

            xhr = null;
          } else {
            setTimeout(ready, 10);
          }
        };

        // Default settings
        settings.scope = settings.scope || this;
        settings.success_scope = settings.success_scope || settings.scope;
        settings.error_scope = settings.error_scope || settings.scope;
        settings.async = settings.async === false ? false : true;
        settings.data = settings.data || '';

        XHR.fire('beforeInitialize', { settings: settings });

        xhr = new XMLHttpRequest();

        if (xhr) {
          if (xhr.overrideMimeType) {
            xhr.overrideMimeType(settings.content_type);
          }

          xhr.open(settings.type || (settings.data ? 'POST' : 'GET'), settings.url, settings.async);

          if (settings.crossDomain) {
            xhr.withCredentials = true;
          }

          if (settings.content_type) {
            xhr.setRequestHeader('Content-Type', settings.content_type);
          }

          if (settings.requestheaders) {
            Tools.each(settings.requestheaders, function (header) {
              xhr.setRequestHeader(header.key, header.value);
            });
          }

          xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');

          xhr = XHR.fire('beforeSend', { xhr: xhr, settings: settings }).xhr;
          xhr.send(settings.data);

          // Syncronous request
          if (!settings.async) {
            return ready();
          }

          // Wait for response, onReadyStateChange can not be used since it leaks memory in IE
          setTimeout(ready, 10);
        }
      }
    };

    Tools.extend(XHR, Observable);

    return XHR;
  }
);

/**
 * JSONRequest.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
 */

/**
 * This class enables you to use JSON-RPC to call backend methods.
 *
 * @class tinymce.util.JSONRequest
 * @example
 * var json = new tinymce.util.JSONRequest({
 *     url: 'somebackend.php'
 * });
 *
 * // Send RPC call 1
 * json.send({
 *     method: 'someMethod1',
 *     params: ['a', 'b'],
 *     success: function(result) {
 *         console.dir(result);
 *     }
 * });
 *
 * // Send RPC call 2
 * json.send({
 *     method: 'someMethod2',
 *     params: ['a', 'b'],
 *     success: function(result) {
 *         console.dir(result);
 *     }
 * });
 */
define(
  'tinymce.core.util.JSONRequest',
  [
    "tinymce.core.util.JSON",
    "tinymce.core.util.XHR",
    "tinymce.core.util.Tools"
  ],
  function (JSON, XHR, Tools) {
    var extend = Tools.extend;

    var JSONRequest = function (settings) {
      this.settings = extend({}, settings);
      this.count = 0;
    };

    /**
     * Simple helper function to send a JSON-RPC request without the need to initialize an object.
     * Consult the Wiki API documentation for more details on what you can pass to this function.
     *
     * @method sendRPC
     * @static
     * @param {Object} o Call object where there are three field id, method and params this object should also contain callbacks etc.
     */
    JSONRequest.sendRPC = function (o) {
      return new JSONRequest().send(o);
    };

    JSONRequest.prototype = {
      /**
       * Sends a JSON-RPC call. Consult the Wiki API documentation for more details on what you can pass to this function.
       *
       * @method send
       * @param {Object} args Call object where there are three field id, method and params this object should also contain callbacks etc.
       */
      send: function (args) {
        var ecb = args.error, scb = args.success;

        args = extend(this.settings, args);

        args.success = function (c, x) {
          c = JSON.parse(c);

          if (typeof c == 'undefined') {
            c = {
              error: 'JSON Parse error.'
            };
          }

          if (c.error) {
            ecb.call(args.error_scope || args.scope, c.error, x);
          } else {
            scb.call(args.success_scope || args.scope, c.result);
          }
        };

        args.error = function (ty, x) {
          if (ecb) {
            ecb.call(args.error_scope || args.scope, ty, x);
          }
        };

        args.data = JSON.serialize({
          id: args.id || 'c' + (this.count++),
          method: args.method,
          params: args.params
        });

        // JSON content type for Ruby on rails. Bug: #1883287
        args.content_type = 'application/json';

        XHR.send(args);
      }
    };

    return JSONRequest;
  }
);
/**
 * LocalStorage.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
 */

/**
 * This class will simulate LocalStorage on IE 7 and return the native version on modern browsers.
 * Storage is done using userData on IE 7 and a special serialization format. The format is designed
 * to be as small as possible by making sure that the keys and values doesn't need to be encoded. This
 * makes it possible to store for example HTML data.
 *
 * Storage format for userData:
 * <base 32 key length>,<key string>,<base 32 value length>,<value>,...
 *
 * For example this data key1=value1,key2=value2 would be:
 * 4,key1,6,value1,4,key2,6,value2
 *
 * @class tinymce.util.LocalStorage
 * @static
 * @version 4.0
 * @example
 * tinymce.util.LocalStorage.setItem('key', 'value');
 * var value = tinymce.util.LocalStorage.getItem('key');
 */
define(
  'tinymce.core.util.LocalStorage',
  [
    'global!document',
    'global!window'
  ],
  function (document, window) {
    var LocalStorage, storageElm, items, keys, userDataKey, hasOldIEDataSupport;

    // Check for native support
    try {
      if (window.localStorage) {
        return window.localStorage;
      }
    } catch (ex) {
      // Ignore
    }

    userDataKey = "tinymce";
    storageElm = document.documentElement;
    hasOldIEDataSupport = !!storageElm.addBehavior;

    if (hasOldIEDataSupport) {
      storageElm.addBehavior('#default#userData');
    }

    /**
     * Gets the keys names and updates LocalStorage.length property. Since IE7 doesn't have any getters/setters.
     */
    var updateKeys = function () {
      keys = [];

      for (var key in items) {
        keys.push(key);
      }

      LocalStorage.length = keys.length;
    };

    /**
     * Loads the userData string and parses it into the items structure.
     */
    var load = function () {
      var key, data, value, pos = 0;

      items = {};

      // localStorage can be disabled on WebKit/Gecko so make a dummy storage
      if (!hasOldIEDataSupport) {
        return;
      }

      var next = function (end) {
        var value, nextPos;

        nextPos = end !== undefined ? pos + end : data.indexOf(',', pos);
        if (nextPos === -1 || nextPos > data.length) {
          return null;
        }

        value = data.substring(pos, nextPos);
        pos = nextPos + 1;

        return value;
      };

      storageElm.load(userDataKey);
      data = storageElm.getAttribute(userDataKey) || '';

      do {
        var offset = next();
        if (offset === null) {
          break;
        }

        key = next(parseInt(offset, 32) || 0);
        if (key !== null) {
          offset = next();
          if (offset === null) {
            break;
          }

          value = next(parseInt(offset, 32) || 0);

          if (key) {
            items[key] = value;
          }
        }
      } while (key !== null);

      updateKeys();
    };

    /**
     * Saves the items structure into a the userData format.
     */
    var save = function () {
      var value, data = '';

      // localStorage can be disabled on WebKit/Gecko so make a dummy storage
      if (!hasOldIEDataSupport) {
        return;
      }

      for (var key in items) {
        value = items[key];
        data += (data ? ',' : '') + key.length.toString(32) + ',' + key + ',' + value.length.toString(32) + ',' + value;
      }

      storageElm.setAttribute(userDataKey, data);

      try {
        storageElm.save(userDataKey);
      } catch (ex) {
        // Ignore disk full
      }

      updateKeys();
    };

    LocalStorage = {
      /**
       * Length of the number of items in storage.
       *
       * @property length
       * @type Number
       * @return {Number} Number of items in storage.
       */
      //length:0,

      /**
       * Returns the key name by index.
       *
       * @method key
       * @param {Number} index Index of key to return.
       * @return {String} Key value or null if it wasn't found.
       */
      key: function (index) {
        return keys[index];
      },

      /**
       * Returns the value if the specified key or null if it wasn't found.
       *
       * @method getItem
       * @param {String} key Key of item to retrieve.
       * @return {String} Value of the specified item or null if it wasn't found.
       */
      getItem: function (key) {
        return key in items ? items[key] : null;
      },

      /**
       * Sets the value of the specified item by it's key.
       *
       * @method setItem
       * @param {String} key Key of the item to set.
       * @param {String} value Value of the item to set.
       */
      setItem: function (key, value) {
        items[key] = "" + value;
        save();
      },

      /**
       * Removes the specified item by key.
       *
       * @method removeItem
       * @param {String} key Key of item to remove.
       */
      removeItem: function (key) {
        delete items[key];
        save();
      },

      /**
       * Removes all items.
       *
       * @method clear
       */
      clear: function () {
        items = {};
        save();
      }
    };

    load();

    return LocalStorage;
  }
);

/**
 * Tinymce.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
 */

define(
  'tinymce.core.api.Tinymce',
  [
    'tinymce.core.AddOnManager',
    'tinymce.core.api.Formatter',
    'tinymce.core.api.NotificationManager',
    'tinymce.core.api.WindowManager',
    'tinymce.core.dom.BookmarkManager',
    'tinymce.core.dom.ControlSelection',
    'tinymce.core.dom.DomQuery',
    'tinymce.core.dom.DOMUtils',
    'tinymce.core.dom.EventUtils',
    'tinymce.core.dom.RangeUtils',
    'tinymce.core.dom.ScriptLoader',
    'tinymce.core.dom.Selection',
    'tinymce.core.dom.Serializer',
    'tinymce.core.dom.Sizzle',
    'tinymce.core.dom.TreeWalker',
    'tinymce.core.Editor',
    'tinymce.core.EditorCommands',
    'tinymce.core.EditorManager',
    'tinymce.core.EditorObservable',
    'tinymce.core.Env',
    'tinymce.core.FocusManager',
    'tinymce.core.geom.Rect',
    'tinymce.core.html.DomParser',
    'tinymce.core.html.Entities',
    'tinymce.core.html.Node',
    'tinymce.core.html.SaxParser',
    'tinymce.core.html.Schema',
    'tinymce.core.html.Serializer',
    'tinymce.core.html.Styles',
    'tinymce.core.html.Writer',
    'tinymce.core.Shortcuts',
    'tinymce.core.ui.Factory',
    'tinymce.core.UndoManager',
    'tinymce.core.util.Class',
    'tinymce.core.util.Color',
    'tinymce.core.util.Delay',
    'tinymce.core.util.EventDispatcher',
    'tinymce.core.util.I18n',
    'tinymce.core.util.JSON',
    'tinymce.core.util.JSONP',
    'tinymce.core.util.JSONRequest',
    'tinymce.core.util.LocalStorage',
    'tinymce.core.util.Observable',
    'tinymce.core.util.Promise',
    'tinymce.core.util.Tools',
    'tinymce.core.util.URI',
    'tinymce.core.util.VK',
    'tinymce.core.util.XHR'
  ],
  function (
    AddOnManager, Formatter, NotificationManager, WindowManager, BookmarkManager, ControlSelection, DomQuery, DOMUtils, EventUtils, RangeUtils, ScriptLoader,
    Selection, DomSerializer, Sizzle, TreeWalker, Editor, EditorCommands, EditorManager, EditorObservable, Env, FocusManager, Rect, DomParser, Entities, Node,
    SaxParser, Schema, HtmlSerializer, Styles, Writer, Shortcuts, Factory, UndoManager, Class, Color, Delay, EventDispatcher, I18n, JSON, JSONP, JSONRequest,
    LocalStorage, Observable, Promise, Tools, URI, VK, XHR
  ) {
    var tinymce = EditorManager;

    /**
     * @include ../../../../../../tools/docs/tinymce.js
     */
    var publicApi = {
      geom: {
        Rect: Rect
      },

      util: {
        Promise: Promise,
        Delay: Delay,
        Tools: Tools,
        VK: VK,
        URI: URI,
        Class: Class,
        EventDispatcher: EventDispatcher,
        Observable: Observable,
        I18n: I18n,
        XHR: XHR,
        JSON: JSON,
        JSONRequest: JSONRequest,
        JSONP: JSONP,
        LocalStorage: LocalStorage,
        Color: Color
      },

      dom: {
        EventUtils: EventUtils,
        Sizzle: Sizzle,
        DomQuery: DomQuery,
        TreeWalker: TreeWalker,
        DOMUtils: DOMUtils,
        ScriptLoader: ScriptLoader,
        RangeUtils: RangeUtils,
        Serializer: DomSerializer,
        ControlSelection: ControlSelection,
        BookmarkManager: BookmarkManager,
        Selection: Selection,
        Event: EventUtils.Event
      },

      html: {
        Styles: Styles,
        Entities: Entities,
        Node: Node,
        Schema: Schema,
        SaxParser: SaxParser,
        DomParser: DomParser,
        Writer: Writer,
        Serializer: HtmlSerializer
      },

      ui: {
        Factory: Factory
      },

      Env: Env,
      AddOnManager: AddOnManager,
      Formatter: Formatter,
      UndoManager: UndoManager,
      EditorCommands: EditorCommands,
      WindowManager: WindowManager,
      NotificationManager: NotificationManager,
      EditorObservable: EditorObservable,
      Shortcuts: Shortcuts,
      Editor: Editor,
      FocusManager: FocusManager,
      EditorManager: EditorManager,

      // Global instances
      DOM: DOMUtils.DOM,
      ScriptLoader: ScriptLoader.ScriptLoader,
      PluginManager: AddOnManager.PluginManager,
      ThemeManager: AddOnManager.ThemeManager,

      // Global utility functions
      trim: Tools.trim,
      isArray: Tools.isArray,
      is: Tools.is,
      toArray: Tools.toArray,
      makeMap: Tools.makeMap,
      each: Tools.each,
      map: Tools.map,
      grep: Tools.grep,
      inArray: Tools.inArray,
      extend: Tools.extend,
      create: Tools.create,
      walk: Tools.walk,
      createNS: Tools.createNS,
      resolve: Tools.resolve,
      explode: Tools.explode,
      _addCacheSuffix: Tools._addCacheSuffix,

      // Legacy browser detection
      isOpera: Env.opera,
      isWebKit: Env.webkit,
      isIE: Env.ie,
      isGecko: Env.gecko,
      isMac: Env.mac
    };

    tinymce = Tools.extend(tinymce, publicApi);

    return tinymce;
  }
);

/**
 * Main.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
 */

define(
  'tinymce.core.api.Main',
  [
    'ephox.katamari.api.Fun',
    'global!window',
    'tinymce.core.api.Tinymce'
  ],
  function (Fun, window, Tinymce) {
    /*eslint consistent-this: 0 */
    var context = this || window;

    var exportToModuleLoaders = function (tinymce) {
      // Bolt
      if (typeof context.define === "function" && !context.define.amd) {
        context.define("ephox/tinymce", [], Fun.constant(tinymce));
        context.define("tinymce.core.EditorManager", [], Fun.constant(tinymce));
      }

      // CommonJS
      if (typeof module === 'object') {
        /* global module */
        module.exports = tinymce;
      }
    };

    var exportToWindowGlobal = function (tinymce) {
      window.tinymce = tinymce;
      window.tinyMCE = tinymce;
    };

    return function () {
      exportToWindowGlobal(Tinymce);
      exportToModuleLoaders(Tinymce);
      return Tinymce;
    };
  }
);

dem('tinymce.core.api.Main')();
})();
