Skip to content

Instantly share code, notes, and snippets.

@thedavidmeister
Last active December 20, 2017 14:20
Show Gist options
  • Save thedavidmeister/f55b91c8e5e0c0e64e11e0c25f0ff3fe to your computer and use it in GitHub Desktop.
Save thedavidmeister/f55b91c8e5e0c0e64e11e0c25f0ff3fe to your computer and use it in GitHub Desktop.
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["BalloonEditor"] = factory();
else
root["BalloonEditor"] = factory();
})(typeof self !== 'undefined' ? self : this, function() {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, {
/******/ configurable: false,
/******/ enumerable: true,
/******/ get: getter
/******/ });
/******/ }
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 4);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports) {
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
// css base code, injected by the css-loader
module.exports = function(useSourceMap) {
var list = [];
// return the list of modules as css string
list.toString = function toString() {
return this.map(function (item) {
var content = cssWithMappingToString(item, useSourceMap);
if(item[2]) {
return "@media " + item[2] + "{" + content + "}";
} else {
return content;
}
}).join("");
};
// import a list of modules into the list
list.i = function(modules, mediaQuery) {
if(typeof modules === "string")
modules = [[null, modules, ""]];
var alreadyImportedModules = {};
for(var i = 0; i < this.length; i++) {
var id = this[i][0];
if(typeof id === "number")
alreadyImportedModules[id] = true;
}
for(i = 0; i < modules.length; i++) {
var item = modules[i];
// skip already imported module
// this implementation is not 100% perfect for weird media query combinations
// when a module is imported multiple times with different media queries.
// I hope this will never occur (Hey this way we have smaller bundles)
if(typeof item[0] !== "number" || !alreadyImportedModules[item[0]]) {
if(mediaQuery && !item[2]) {
item[2] = mediaQuery;
} else if(mediaQuery) {
item[2] = "(" + item[2] + ") and (" + mediaQuery + ")";
}
list.push(item);
}
}
};
return list;
};
function cssWithMappingToString(item, useSourceMap) {
var content = item[1] || '';
var cssMapping = item[3];
if (!cssMapping) {
return content;
}
if (useSourceMap && typeof btoa === 'function') {
var sourceMapping = toComment(cssMapping);
var sourceURLs = cssMapping.sources.map(function (source) {
return '/*# sourceURL=' + cssMapping.sourceRoot + source + ' */'
});
return [content].concat(sourceURLs).concat([sourceMapping]).join('\n');
}
return [content].join('\n');
}
// Adapted from convert-source-map (MIT)
function toComment(sourceMap) {
// eslint-disable-next-line no-undef
var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))));
var data = 'sourceMappingURL=data:application/json;charset=utf-8;base64,' + base64;
return '/*# ' + data + ' */';
}
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
var stylesInDom = {};
var memoize = function (fn) {
var memo;
return function () {
if (typeof memo === "undefined") memo = fn.apply(this, arguments);
return memo;
};
};
var isOldIE = memoize(function () {
// Test for IE <= 9 as proposed by Browserhacks
// @see http://browserhacks.com/#hack-e71d8692f65334173fee715c222cb805
// Tests for existence of standard globals is to allow style-loader
// to operate correctly into non-standard environments
// @see https://github.com/webpack-contrib/style-loader/issues/177
return window && document && document.all && !window.atob;
});
var getElement = (function (fn) {
var memo = {};
return function(selector) {
if (typeof memo[selector] === "undefined") {
memo[selector] = fn.call(this, selector);
}
return memo[selector]
};
})(function (target) {
return document.querySelector(target)
});
var singleton = null;
var singletonCounter = 0;
var stylesInsertedAtTop = [];
var fixUrls = __webpack_require__(11);
module.exports = function(list, options) {
if (typeof DEBUG !== "undefined" && DEBUG) {
if (typeof document !== "object") throw new Error("The style-loader cannot be used in a non-browser environment");
}
options = options || {};
options.attrs = typeof options.attrs === "object" ? options.attrs : {};
// Force single-tag solution on IE6-9, which has a hard limit on the # of <style>
// tags it will allow on a page
if (!options.singleton) options.singleton = isOldIE();
// By default, add <style> tags to the <head> element
if (!options.insertInto) options.insertInto = "head";
// By default, add <style> tags to the bottom of the target
if (!options.insertAt) options.insertAt = "bottom";
var styles = listToStyles(list, options);
addStylesToDom(styles, options);
return function update (newList) {
var mayRemove = [];
for (var i = 0; i < styles.length; i++) {
var item = styles[i];
var domStyle = stylesInDom[item.id];
domStyle.refs--;
mayRemove.push(domStyle);
}
if(newList) {
var newStyles = listToStyles(newList, options);
addStylesToDom(newStyles, options);
}
for (var i = 0; i < mayRemove.length; i++) {
var domStyle = mayRemove[i];
if(domStyle.refs === 0) {
for (var j = 0; j < domStyle.parts.length; j++) domStyle.parts[j]();
delete stylesInDom[domStyle.id];
}
}
};
};
function addStylesToDom (styles, options) {
for (var i = 0; i < styles.length; i++) {
var item = styles[i];
var domStyle = stylesInDom[item.id];
if(domStyle) {
domStyle.refs++;
for(var j = 0; j < domStyle.parts.length; j++) {
domStyle.parts[j](item.parts[j]);
}
for(; j < item.parts.length; j++) {
domStyle.parts.push(addStyle(item.parts[j], options));
}
} else {
var parts = [];
for(var j = 0; j < item.parts.length; j++) {
parts.push(addStyle(item.parts[j], options));
}
stylesInDom[item.id] = {id: item.id, refs: 1, parts: parts};
}
}
}
function listToStyles (list, options) {
var styles = [];
var newStyles = {};
for (var i = 0; i < list.length; i++) {
var item = list[i];
var id = options.base ? item[0] + options.base : item[0];
var css = item[1];
var media = item[2];
var sourceMap = item[3];
var part = {css: css, media: media, sourceMap: sourceMap};
if(!newStyles[id]) styles.push(newStyles[id] = {id: id, parts: [part]});
else newStyles[id].parts.push(part);
}
return styles;
}
function insertStyleElement (options, style) {
var target = getElement(options.insertInto)
if (!target) {
throw new Error("Couldn't find a style target. This probably means that the value for the 'insertInto' parameter is invalid.");
}
var lastStyleElementInsertedAtTop = stylesInsertedAtTop[stylesInsertedAtTop.length - 1];
if (options.insertAt === "top") {
if (!lastStyleElementInsertedAtTop) {
target.insertBefore(style, target.firstChild);
} else if (lastStyleElementInsertedAtTop.nextSibling) {
target.insertBefore(style, lastStyleElementInsertedAtTop.nextSibling);
} else {
target.appendChild(style);
}
stylesInsertedAtTop.push(style);
} else if (options.insertAt === "bottom") {
target.appendChild(style);
} else {
throw new Error("Invalid value for parameter 'insertAt'. Must be 'top' or 'bottom'.");
}
}
function removeStyleElement (style) {
if (style.parentNode === null) return false;
style.parentNode.removeChild(style);
var idx = stylesInsertedAtTop.indexOf(style);
if(idx >= 0) {
stylesInsertedAtTop.splice(idx, 1);
}
}
function createStyleElement (options) {
var style = document.createElement("style");
options.attrs.type = "text/css";
addAttrs(style, options.attrs);
insertStyleElement(options, style);
return style;
}
function createLinkElement (options) {
var link = document.createElement("link");
options.attrs.type = "text/css";
options.attrs.rel = "stylesheet";
addAttrs(link, options.attrs);
insertStyleElement(options, link);
return link;
}
function addAttrs (el, attrs) {
Object.keys(attrs).forEach(function (key) {
el.setAttribute(key, attrs[key]);
});
}
function addStyle (obj, options) {
var style, update, remove, result;
// If a transform function was defined, run it on the css
if (options.transform && obj.css) {
result = options.transform(obj.css);
if (result) {
// If transform returns a value, use that instead of the original css.
// This allows running runtime transformations on the css.
obj.css = result;
} else {
// If the transform function returns a falsy value, don't add this css.
// This allows conditional loading of css
return function() {
// noop
};
}
}
if (options.singleton) {
var styleIndex = singletonCounter++;
style = singleton || (singleton = createStyleElement(options));
update = applyToSingletonTag.bind(null, style, styleIndex, false);
remove = applyToSingletonTag.bind(null, style, styleIndex, true);
} else if (
obj.sourceMap &&
typeof URL === "function" &&
typeof URL.createObjectURL === "function" &&
typeof URL.revokeObjectURL === "function" &&
typeof Blob === "function" &&
typeof btoa === "function"
) {
style = createLinkElement(options);
update = updateLink.bind(null, style, options);
remove = function () {
removeStyleElement(style);
if(style.href) URL.revokeObjectURL(style.href);
};
} else {
style = createStyleElement(options);
update = applyToTag.bind(null, style);
remove = function () {
removeStyleElement(style);
};
}
update(obj);
return function updateStyle (newObj) {
if (newObj) {
if (
newObj.css === obj.css &&
newObj.media === obj.media &&
newObj.sourceMap === obj.sourceMap
) {
return;
}
update(obj = newObj);
} else {
remove();
}
};
}
var replaceText = (function () {
var textStore = [];
return function (index, replacement) {
textStore[index] = replacement;
return textStore.filter(Boolean).join('\n');
};
})();
function applyToSingletonTag (style, index, remove, obj) {
var css = remove ? "" : obj.css;
if (style.styleSheet) {
style.styleSheet.cssText = replaceText(index, css);
} else {
var cssNode = document.createTextNode(css);
var childNodes = style.childNodes;
if (childNodes[index]) style.removeChild(childNodes[index]);
if (childNodes.length) {
style.insertBefore(cssNode, childNodes[index]);
} else {
style.appendChild(cssNode);
}
}
}
function applyToTag (style, obj) {
var css = obj.css;
var media = obj.media;
if(media) {
style.setAttribute("media", media)
}
if(style.styleSheet) {
style.styleSheet.cssText = css;
} else {
while(style.firstChild) {
style.removeChild(style.firstChild);
}
style.appendChild(document.createTextNode(css));
}
}
function updateLink (link, options, obj) {
var css = obj.css;
var sourceMap = obj.sourceMap;
/*
If convertToAbsoluteUrls isn't defined, but sourcemaps are enabled
and there is no publicPath defined then lets turn convertToAbsoluteUrls
on by default. Otherwise default to the convertToAbsoluteUrls option
directly
*/
var autoFixUrls = options.convertToAbsoluteUrls === undefined && sourceMap;
if (options.convertToAbsoluteUrls || autoFixUrls) {
css = fixUrls(css);
}
if (sourceMap) {
// http://stackoverflow.com/a/26603875
css += "\n/*# sourceMappingURL=data:application/json;base64," + btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))) + " */";
}
var blob = new Blob([css], { type: "text/css" });
var oldSrc = link.href;
link.href = URL.createObjectURL(blob);
if(oldSrc) URL.revokeObjectURL(oldSrc);
}
/***/ }),
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* WEBPACK VAR INJECTION */(function(module, global) {/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__checkGlobal__ = __webpack_require__(6);
/** Used to determine if values are of the language type `Object`. */
var objectTypes = {
'function': true,
'object': true
};
/** Detect free variable `exports`. */
var freeExports = (objectTypes[typeof exports] && exports && !exports.nodeType)
? exports
: undefined;
/** Detect free variable `module`. */
var freeModule = (objectTypes[typeof module] && module && !module.nodeType)
? module
: undefined;
/** Detect free variable `global` from Node.js. */
var freeGlobal = Object(__WEBPACK_IMPORTED_MODULE_0__checkGlobal__["a" /* default */])(freeExports && freeModule && typeof global == 'object' && global);
/** Detect free variable `self`. */
var freeSelf = Object(__WEBPACK_IMPORTED_MODULE_0__checkGlobal__["a" /* default */])(objectTypes[typeof self] && self);
/** Detect free variable `window`. */
var freeWindow = Object(__WEBPACK_IMPORTED_MODULE_0__checkGlobal__["a" /* default */])(objectTypes[typeof window] && window);
/** Detect `this` as the global object. */
var thisGlobal = Object(__WEBPACK_IMPORTED_MODULE_0__checkGlobal__["a" /* default */])(objectTypes[typeof this] && this);
/**
* Used as a reference to the global object.
*
* The `this` value is used if it's the global object to avoid Greasemonkey's
* restricted `window` object, otherwise the `window` object is used.
*/
var root = freeGlobal ||
((freeWindow !== (thisGlobal && thisGlobal.window)) && freeWindow) ||
freeSelf || thisGlobal || Function('return this')();
/* harmony default export */ __webpack_exports__["a"] = (root);
/* WEBPACK VAR INJECTION */}.call(__webpack_exports__, __webpack_require__(3)(module), __webpack_require__(5)))
/***/ }),
/* 3 */
/***/ (function(module, exports) {
module.exports = function(originalModule) {
if(!originalModule.webpackPolyfill) {
var module = Object.create(originalModule);
// module.parent = undefined by default
if(!module.children) module.children = [];
Object.defineProperty(module, "loaded", {
enumerable: true,
get: function() {
return module.l;
}
});
Object.defineProperty(module, "id", {
enumerable: true,
get: function() {
return module.i;
}
});
Object.defineProperty(module, "exports", {
enumerable: true,
});
module.webpackPolyfill = 1;
}
return module;
};
/***/ }),
/* 4 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_getPrototype.js
/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeGetPrototype = Object.getPrototypeOf;
/**
* Gets the `[[Prototype]]` of `value`.
*
* @private
* @param {*} value The value to query.
* @returns {null|Object} Returns the `[[Prototype]]`.
*/
function getPrototype(value) {
return nativeGetPrototype(Object(value));
}
/* harmony default export */ var _getPrototype = (getPrototype);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_isHostObject.js
/**
* Checks if `value` is a host object in IE < 9.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a host object, else `false`.
*/
function isHostObject(value) {
// Many host objects are `Object` objects that can coerce to strings
// despite having improperly defined `toString` methods.
var result = false;
if (value != null && typeof value.toString != 'function') {
try {
result = !!(value + '');
} catch (e) {}
}
return result;
}
/* harmony default export */ var _isHostObject = (isHostObject);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isObjectLike.js
/**
* Checks if `value` is object-like. A value is object-like if it's not `null`
* and has a `typeof` result of "object".
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is object-like, else `false`.
* @example
*
* _.isObjectLike({});
* // => true
*
* _.isObjectLike([1, 2, 3]);
* // => true
*
* _.isObjectLike(_.noop);
* // => false
*
* _.isObjectLike(null);
* // => false
*/
function isObjectLike(value) {
return !!value && typeof value == 'object';
}
/* harmony default export */ var lodash_isObjectLike = (isObjectLike);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isPlainObject.js
/** `Object#toString` result references. */
var objectTag = '[object Object]';
/** Used for built-in method references. */
var objectProto = Object.prototype;
/** Used to resolve the decompiled source of functions. */
var funcToString = Function.prototype.toString;
/** Used to check objects for own properties. */
var isPlainObject_hasOwnProperty = objectProto.hasOwnProperty;
/** Used to infer the `Object` constructor. */
var objectCtorString = funcToString.call(Object);
/**
* Used to resolve the
* [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring)
* of values.
*/
var objectToString = objectProto.toString;
/**
* Checks if `value` is a plain object, that is, an object created by the
* `Object` constructor or one with a `[[Prototype]]` of `null`.
*
* @static
* @memberOf _
* @since 0.8.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a plain object,
* else `false`.
* @example
*
* function Foo() {
* this.a = 1;
* }
*
* _.isPlainObject(new Foo);
* // => false
*
* _.isPlainObject([1, 2, 3]);
* // => false
*
* _.isPlainObject({ 'x': 0, 'y': 0 });
* // => true
*
* _.isPlainObject(Object.create(null));
* // => true
*/
function isPlainObject(value) {
if (!lodash_isObjectLike(value) ||
objectToString.call(value) != objectTag || _isHostObject(value)) {
return false;
}
var proto = _getPrototype(value);
if (proto === null) {
return true;
}
var Ctor = isPlainObject_hasOwnProperty.call(proto, 'constructor') && proto.constructor;
return (typeof Ctor == 'function' &&
Ctor instanceof Ctor && funcToString.call(Ctor) == objectCtorString);
}
/* harmony default export */ var lodash_isPlainObject = (isPlainObject);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/config.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module utils/config
*/
/**
* Handles a configuration dictionary.
*/
class config_Config {
/**
* Creates an instance of the {@link ~Config} class.
*
* @param {Object} [configurations] The initial configurations to be set. Usually, provided by the user.
* @param {Object} [defaultConfigurations] The default configurations. Usually, provided by the system.
*/
constructor( configurations, defaultConfigurations ) {
/**
* Store for the whole configuration.
*
* @private
* @member {Object}
*/
this._config = {};
// Set default configuration.
if ( defaultConfigurations ) {
this.define( defaultConfigurations );
}
// Set initial configuration.
if ( configurations ) {
this._setObjectToTarget( this._config, configurations );
}
}
/**
* Set configuration values.
*
* It accepts both a name/value pair or an object, which properties and values will be used to set
* configurations.
*
* It also accepts setting a "deep configuration" by using dots in the name. For example, `'resize.width'` sets
* the value for the `width` configuration in the `resize` subset.
*
* config.set( 'width', 500 );
* config.set( 'toolbar.collapsed', true );
*
* // Equivalent to:
* config.set( {
* width: 500
* toolbar: {
* collapsed: true
* }
* } );
*
* Passing an object as the value will amend the configuration, not replace it.
*
* config.set( 'toolbar', {
* collapsed: true,
* } );
*
* config.set( 'toolbar', {
* color: 'red',
* } );
*
* config.get( 'toolbar.collapsed' ); // true
* config.get( 'toolbar.color' ); // 'red'
*
* @param {String|Object} name The configuration name or an object from which take properties as
* configuration entries. Configuration names are case-sensitive.
* @param {*} value The configuration value. Used if a name is passed.
*/
set( name, value ) {
this._setToTarget( this._config, name, value );
}
/**
* Does exactly the same as {@link #set} with one exception – passed configuration extends
* existing one, but does not overwrite already defined values.
*
* This method is supposed to be called by plugin developers to setup plugin's configurations. It would be
* rarely used for other needs.
*
* @param {String|Object} name The configuration name or an object from which take properties as
* configuration entries. Configuration names are case-sensitive.
* @param {*} value The configuration value. Used if a name is passed.
*/
define( name, value ) {
const isDefine = true;
this._setToTarget( this._config, name, value, isDefine );
}
/**
* Gets the value for a configuration entry.
*
* config.get( 'name' );
*
* Deep configurations can be retrieved by separating each part with a dot.
*
* config.get( 'toolbar.collapsed' );
*
* @param {String} name The configuration name. Configuration names are case-sensitive.
* @returns {*} The configuration value or `undefined` if the configuration entry was not found.
*/
get( name ) {
return this._getFromSource( this._config, name );
}
/**
* Saves passed configuration to the specified target (nested object).
*
* @private
* @param {Object} target Nested config object.
* @param {String|Object} name The configuration name or an object from which take properties as
* configuration entries. Configuration names are case-sensitive.
* @param {*} value The configuration value. Used if a name is passed.
* @param {Boolean} [isDefine=false] Define if passed configuration should overwrite existing one.
*/
_setToTarget( target, name, value, isDefine = false ) {
// In case of an object, iterate through it and call `_setToTarget` again for each property.
if ( lodash_isPlainObject( name ) ) {
this._setObjectToTarget( target, name, isDefine );
return;
}
// The configuration name should be split into parts if it has dots. E.g. `resize.width` -> [`resize`, `width`].
const parts = name.split( '.' );
// Take the name of the configuration out of the parts. E.g. `resize.width` -> `width`.
name = parts.pop();
// Iterate over parts to check if currently stored configuration has proper structure.
for ( const part of parts ) {
// If there is no object for specified part then create one.
if ( !lodash_isPlainObject( target[ part ] ) ) {
target[ part ] = {};
}
// Nested object becomes a target.
target = target[ part ];
}
// In case of value is an object.
if ( lodash_isPlainObject( value ) ) {
// We take care of proper config structure.
if ( !lodash_isPlainObject( target[ name ] ) ) {
target[ name ] = {};
}
target = target[ name ];
// And iterate through this object calling `_setToTarget` again for each property.
this._setObjectToTarget( target, value, isDefine );
return;
}
// Do nothing if we are defining configuration for non empty name.
if ( isDefine && typeof target[ name ] != 'undefined' ) {
return;
}
target[ name ] = value;
}
/**
* Get specified configuration from specified source (nested object).
*
* @private
* @param {Object} source level of nested object.
* @param {String} name The configuration name. Configuration names are case-sensitive.
* @returns {*} The configuration value or `undefined` if the configuration entry was not found.
*/
_getFromSource( source, name ) {
// The configuration name should be split into parts if it has dots. E.g. `resize.width` -> [`resize`, `width`].
const parts = name.split( '.' );
// Take the name of the configuration out of the parts. E.g. `resize.width` -> `width`.
name = parts.pop();
// Iterate over parts to check if currently stored configuration has proper structure.
for ( const part of parts ) {
if ( !lodash_isPlainObject( source[ part ] ) ) {
source = null;
break;
}
// Nested object becomes a source.
source = source[ part ];
}
// Always returns undefined for non existing configuration
return source ? source[ name ] : undefined;
}
/**
* Iterates through passed object and calls {@link #_setToTarget} method with object key and value for each property.
*
* @private
* @param {Object} target Nested config object.
* @param {Object} configuration Configuration data set
* @param {Boolean} [isDefine] Defines if passed configuration is default configuration or not.
*/
_setObjectToTarget( target, configuration, isDefine ) {
Object.keys( configuration ).forEach( key => {
this._setToTarget( target, key, configuration[ key ], isDefine );
} );
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module utils/ckeditorerror
*/
/**
* URL to the documentation with error codes.
*/
const DOCUMENTATION_URL =
'https://ckeditor5.github.io/docs/nightly/ckeditor5/latest/framework/guides/support/error-codes.html';
/**
* The CKEditor error class.
*
* All errors will be shortened during the minification process in order to reduce the code size.
* Therefore, all error messages should be documented in the same way as those in {@link module:utils/log}.
*
* Read more in the {@link module:utils/log} module.
*
* @extends Error
*/
class CKEditorError extends Error {
/**
* Creates an instance of the CKEditorError class.
*
* Read more about error logging in the {@link module:utils/log} module.
*
* @param {String} message The error message in an `error-name: Error message.` format.
* During the minification process the "Error message" part will be removed to limit the code size
* and a link to this error documentation will be added to the `message`.
* @param {Object} [data] Additional data describing the error. A stringified version of this object
* will be appended to the error message, so the data are quickly visible in the console. The original
* data object will also be later available under the {@link #data} property.
*/
constructor( message, data ) {
message = attachLinkToDocumentation( message );
if ( data ) {
message += ' ' + JSON.stringify( data );
}
super( message );
/**
* @member {String}
*/
this.name = 'CKEditorError';
/**
* The additional error data passed to the constructor.
*
* @member {Object}
*/
this.data = data;
}
/**
* Checks if error is an instance of CKEditorError class.
*
* @param {Object} error Object to check.
* @returns {Boolean}
*/
static isCKEditorError( error ) {
return error instanceof CKEditorError;
}
}
/**
* Attaches link to the documentation at the end of the error message.
*
* @param {String} message Message to be logged.
* @returns {String}
*/
function attachLinkToDocumentation( message ) {
const matchedErrorName = message.match( /^([^:]+):/ );
if ( !matchedErrorName ) {
return message;
}
return message + ` Read more: ${ DOCUMENTATION_URL }#${ matchedErrorName[ 1 ] }\n`;
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/log.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/* global console */
/**
* @module utils/log
*/
/**
* The logging module.
*
* This object features two functions that should be used across CKEditor code base to log errors and warnings.
* Despite being an overridable interface for native `console.*` this module serves also the goal to limit the
* code size of a minified CKEditor package. During minification process the messages will be shortened and
* links to their documentation will be logged to the console.
*
* All errors and warning should be documented in the following way:
*
* /**
* * Error thrown when a plugin cannot be loaded due to JavaScript errors, lack of plugins with a given name, etc.
* *
* * @error plugin-load
* * @param pluginName The name of the plugin that could not be loaded.
* * @param moduleName The name of the module which tried to load this plugin.
* * /
* log.error( 'plugin-load: It was not possible to load the "{$pluginName}" plugin in module "{$moduleName}', {
* pluginName: 'foo',
* moduleName: 'bar'
* } );
*
* ### Warning vs Error vs Throw
*
* * Whenever a potentially incorrect situation occurs, which does not directly lead to an incorrect behavior,
* log a warning.
* * Whenever an incorrect situation occurs, but the app may continue working (although perhaps incorrectly),
* log an error.
* * Whenever it's really bad and it does not make sense to continue working, throw a {@link module:utils/ckeditorerror~CKEditorError}.
*
* @namespace
*/
const log = {
/**
* Logs an error to the console.
*
* Read more about error logging in the {@link module:utils/log} module.
*
* @param {String} message The error message in an `error-name: Error message.` format.
* During the minification process the "Error message" part will be removed to limit the code size
* and a link to this error documentation will be logged to the console.
* @param {Object} [data] Additional data describing the error.
*/
error( message, data ) {
console.error( attachLinkToDocumentation( message ), data );
},
/**
* Logs a warning to the console.
*
* Read more about error logging in the {@link module:utils/log} module.
*
* @param {String} message The warning message in a `warning-name: Warning message.` format.
* During the minification process the "Warning message" part will be removed to limit the code size
* and a link to this error documentation will be logged to the console.
* @param {Object} [data] Additional data describing the warning.
*/
warn( message, data ) {
console.warn( attachLinkToDocumentation( message ), data );
}
};
/* harmony default export */ var src_log = (log);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-core/src/plugincollection.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module core/plugincollection
*/
/**
* Manages a list of CKEditor plugins, including loading, resolving dependencies and initialization.
*/
class plugincollection_PluginCollection {
/**
* Creates an instance of the PluginCollection class.
* Allows loading and initializing plugins and their dependencies.
*
* @param {module:core/editor/editor~Editor} editor
* @param {Array.<Function>} [availablePlugins] Plugins (constructors) which the collection will be able to use
* when {@link module:core/plugincollection~PluginCollection#load} is used with plugin names (strings, instead of constructors).
* Usually, the editor will pass its built-in plugins to the collection so they can later be
* used in `config.plugins` or `config.removePlugins` by names.
*/
constructor( editor, availablePlugins = [] ) {
/**
* @protected
* @member {module:core/editor/editor~Editor} module:core/plugin~PluginCollection#_editor
*/
this._editor = editor;
/**
* Map of plugin constructors which can be retrieved by their names.
*
* @protected
* @member {Map.<String|Function,Function>} module:core/plugin~PluginCollection#_availablePlugins
*/
this._availablePlugins = new Map();
/**
* @protected
* @member {Map} module:core/plugin~PluginCollection#_plugins
*/
this._plugins = new Map();
for ( const PluginConstructor of availablePlugins ) {
this._availablePlugins.set( PluginConstructor, PluginConstructor );
if ( PluginConstructor.pluginName ) {
this._availablePlugins.set( PluginConstructor.pluginName, PluginConstructor );
}
}
}
/**
* Collection iterator. Returns `[ PluginConstructor, pluginInstance ]` pairs.
*/
* [ Symbol.iterator ]() {
for ( const entry of this._plugins ) {
if ( typeof entry[ 0 ] == 'function' ) {
yield entry;
}
}
}
/**
* Gets the plugin instance by its constructor or name.
*
* @param {Function|String} key The plugin constructor or {@link module:core/plugin~PluginInterface.pluginName name}.
* @returns {module:core/plugin~PluginInterface}
*/
get( key ) {
return this._plugins.get( key );
}
/**
* Loads a set of plugins and adds them to the collection.
*
* @param {Array.<Function|String>} plugins An array of {@link module:core/plugin~PluginInterface plugin constructors}
* or {@link module:core/plugin~PluginInterface.pluginName plugin names}. The second option (names) work only if
* `availablePlugins` were passed to the {@link #constructor}.
* @param {Array.<String|Function>} [removePlugins] Names of plugins or plugin constructors
* which should not be loaded (despite being specified in the `plugins` array).
* @returns {Promise} A promise which gets resolved once all plugins are loaded and available into the
* collection.
* @returns {Promise.<Array.<module:core/plugin~PluginInterface>>} returns.loadedPlugins The array of loaded plugins.
*/
load( plugins, removePlugins = [] ) {
const that = this;
const editor = this._editor;
const loading = new Set();
const loaded = [];
const pluginConstructors = mapToAvailableConstructors( plugins );
const removePluginConstructors = mapToAvailableConstructors( removePlugins );
const missingPlugins = getMissingPluginNames( plugins );
if ( missingPlugins ) {
// TODO update this error docs with links to docs because it will be a frequent problem.
/**
* Some plugins are not available and could not be loaded.
*
* Plugin classes (constructors) need to be provided to the editor before they can be loaded by name.
* This is usually done by the builder by setting the {@link module:core/editor/editor~Editor.build}
* property.
*
* **If you see this warning when using one of the {@glink builds/index CKEditor 5 Builds}** it means
* that you try to enable a plugin which was not included into that build. This may a be due to a typo
* in the plugin name or simply because that plugin is not part of this build. In the latter scenario,
* read more about {@glink builds/guides/development/custom-builds custom builds}.
*
* **If you see this warning when using one of the editor creators directly** (not a build), then it means
* that you tried loading plugins by name. However, unlike CKEditor 4, CKEditor 5 does not implement a "plugin loader".
* This means that CKEditor 5 does not know where to load the plugin modules from. Therefore, you need to
* provide each plugin through reference (as a constructor function). Check out the examples in
* {@glink builds/guides/integration/advanced-setup#Scenario-2-Building-from-source "Building from source"}.
*
* @error plugincollection-plugin-not-found
* @param {Array.<String>} plugins The name of the plugins which could not be loaded.
*/
const errorMsg = 'plugincollection-plugin-not-found: Some plugins are not available and could not be loaded.';
// Log the error so it's more visible on the console. Hopefully, for better DX.
src_log.error( errorMsg, { plugins: missingPlugins } );
return Promise.reject( new CKEditorError( errorMsg, { plugins: missingPlugins } ) );
}
return Promise.all( pluginConstructors.map( loadPlugin ) )
.then( () => loaded );
function loadPlugin( PluginConstructor ) {
if ( removePluginConstructors.includes( PluginConstructor ) ) {
return;
}
// The plugin is already loaded or being loaded - do nothing.
if ( that.get( PluginConstructor ) || loading.has( PluginConstructor ) ) {
return;
}
return instantiatePlugin( PluginConstructor )
.catch( err => {
/**
* It was not possible to load the plugin.
*
* This is a generic error logged to the console when a JavaSript error is thrown during one of
* the plugins initialization.
*
* If you correctly handled a promise returned by the editor's `create()` method (like shown below)
* you will find the original error logged on the console too:
*
* ClassicEditor.create( document.getElementById( 'editor' ) )
* .then( editor => {
* // ...
* } )
* .catch( error => {
* console.error( error );
* } );
*
* @error plugincollection-load
* @param {String} plugin The name of the plugin that could not be loaded.
*/
src_log.error( 'plugincollection-load: It was not possible to load the plugin.', { plugin: PluginConstructor } );
throw err;
} );
}
function instantiatePlugin( PluginConstructor ) {
return new Promise( resolve => {
loading.add( PluginConstructor );
if ( PluginConstructor.requires ) {
PluginConstructor.requires.forEach( RequiredPluginConstructorOrName => {
const RequiredPluginConstructor = getPluginConstructor( RequiredPluginConstructorOrName );
if ( removePlugins.includes( RequiredPluginConstructor ) ) {
/**
* Cannot load a plugin because one of its dependencies is listed in the `removePlugins` option.
*
* @error plugincollection-required
* @param {Function} plugin The required plugin.
* @param {Function} requiredBy The parent plugin.
*/
throw new CKEditorError(
'plugincollection-required: Cannot load a plugin because one of its dependencies is listed in' +
'the `removePlugins` option.',
{ plugin: RequiredPluginConstructor, requiredBy: PluginConstructor }
);
}
loadPlugin( RequiredPluginConstructor );
} );
}
const plugin = new PluginConstructor( editor );
that._add( PluginConstructor, plugin );
loaded.push( plugin );
resolve();
} );
}
function getPluginConstructor( PluginConstructorOrName ) {
if ( typeof PluginConstructorOrName == 'function' ) {
return PluginConstructorOrName;
}
return that._availablePlugins.get( PluginConstructorOrName );
}
function getMissingPluginNames( plugins ) {
const missingPlugins = [];
for ( const pluginNameOrConstructor of plugins ) {
if ( !getPluginConstructor( pluginNameOrConstructor ) ) {
missingPlugins.push( pluginNameOrConstructor );
}
}
return missingPlugins.length ? missingPlugins : null;
}
function mapToAvailableConstructors( plugins ) {
return plugins
.map( pluginNameOrConstructor => getPluginConstructor( pluginNameOrConstructor ) )
.filter( PluginConstructor => !!PluginConstructor );
}
}
/**
* Destroys all loaded plugins.
*
* @returns {Promise}
*/
destroy() {
const promises = Array.from( this )
.map( ( [ , pluginInstance ] ) => pluginInstance )
.filter( pluginInstance => typeof pluginInstance.destroy == 'function' )
.map( pluginInstance => pluginInstance.destroy() );
return Promise.all( promises );
}
/**
* Adds the plugin to the collection. Exposed mainly for testing purposes.
*
* @protected
* @param {Function} PluginConstructor The plugin constructor.
* @param {module:core/plugin~PluginInterface} plugin The instance of the plugin.
*/
_add( PluginConstructor, plugin ) {
this._plugins.set( PluginConstructor, plugin );
const pluginName = PluginConstructor.pluginName;
if ( !pluginName ) {
return;
}
if ( this._plugins.has( pluginName ) ) {
/**
* Two plugins with the same {@link module:core/plugin~PluginInterface.pluginName} were loaded.
* This may lead to runtime conflicts between these plugins. This usually means that incorrect
* params were passed to {@link module:core/editor/editor~Editor.create}.
*
* @error plugincollection-plugin-name-conflict
* @param {String} pluginName The duplicated plugin name.
* @param {Function} plugin1 The first plugin constructor.
* @param {Function} plugin2 The second plugin constructor.
*/
src_log.warn(
'plugincollection-plugin-name-conflict: Two plugins with the same name were loaded.',
{ pluginName, plugin1: this._plugins.get( pluginName ).constructor, plugin2: PluginConstructor }
);
} else {
this._plugins.set( pluginName, plugin );
}
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-core/src/commandcollection.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module core/commandcollection
*/
/**
* Collection of commands. Its instance is available in {@link module:core/editor/editor~Editor#commands `editor.commands`}.
*/
class commandcollection_CommandCollection {
/**
* Creates collection instance.
*/
constructor() {
/**
* Command map.
*
* @private
* @member {Map}
*/
this._commands = new Map();
}
/**
* Registers a new command.
*
* @param {String} commandName The name of the command.
* @param {module:core/command~Command} command
*/
add( commandName, command ) {
this._commands.set( commandName, command );
}
/**
* Retrieves a command from the collection.
*
* @param {String} commandName The name of the command.
* @returns {module:core/command~Command}
*/
get( commandName ) {
return this._commands.get( commandName );
}
/**
* Executes a command.
*
* @param {String} commandName The name of the command.
*/
execute( commandName, ...args ) {
const command = this.get( commandName );
if ( !command ) {
/**
* Command does not exist.
*
* @error commandcollection-command-not-found
* @param {String} commandName Name of the command.
*/
throw new CKEditorError( 'commandcollection-command-not-found: Command does not exist.', { commandName } );
}
command.execute( ...args );
}
/**
* Returns iterator of command names.
*
* @returns {Iterator.<String>}
*/
* names() {
yield* this._commands.keys();
}
/**
* Returns iterator of command instances.
*
* @returns {Iterator.<module:core/command~Command>}
*/
* commands() {
yield* this._commands.values();
}
/**
* Collection iterator.
*/
[ Symbol.iterator ]() {
return this._commands[ Symbol.iterator ]();
}
/**
* Destroys all collection commands.
*/
destroy() {
for ( const command of this.commands() ) {
command.destroy();
}
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/translation-service.js
/**
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module utils/translation-service
*/
let dictionaries = {};
/**
* Adds package translations to existing ones.
* These translations will later be available for {@link module:utils/translation-service~translate translate}.
*
* add( 'pl', {
* 'OK': 'OK',
* 'Cancel [context: reject]': 'Anuluj'
* } );
*
* @param {String} lang Target language.
* @param {Object.<String, String>} translations Translations which will be added to the dictionary.
*/
function translation_service_add( lang, translations ) {
dictionaries[ lang ] = dictionaries[ lang ] || {};
Object.assign( dictionaries[ lang ], translations );
}
/**
* Translates string if the translation of the string was previously {@link module:utils/translation-service~add added}
* to the dictionary. This happens in a multi-language mode were translation modules are created by the bundler.
*
* When no translation is defined in the dictionary or the dictionary doesn't exist this function returns
* the original string without the `'[context: ]'` (happens in development and single-language modes).
*
* In a single-language mode (when values passed to `t()` were replaced with target languange strings) the dictionary
* is left empty, so this function will return the original strings always.
*
* translate( 'pl', 'Cancel [context: reject]' );
*
* @param {String} lang Target language.
* @param {String} translationKey String which is going to be translated.
* @returns {String} Translated sentence.
*/
function translate( lang, translationKey ) {
if ( !hasTranslation( lang, translationKey ) ) {
return translationKey.replace( / \[context: [^\]]+\]$/, '' );
}
return dictionaries[ lang ][ translationKey ];
}
// Checks whether the dictionary exists and translaiton in that dictionary exists.
function hasTranslation( lang, translationKey ) {
return (
( lang in dictionaries ) &&
( translationKey in dictionaries[ lang ] )
);
}
/**
* Clears dictionaries for test purposes.
*
* @protected
*/
function _clear() {
dictionaries = {};
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/locale.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module utils/locale
*/
/**
* Represents the localization services.
*/
class locale_Locale {
/**
* Creates a new instance of the Locale class.
*
* @param {String} [lang='en'] The language code in [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format.
*/
constructor( lang ) {
/**
* The language code in [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format.
*
* @readonly
* @member {String}
*/
this.lang = lang || 'en';
/**
* Translates the given string to the {@link #lang}. This method is also availble in {@link module:core/editor/editor~Editor#t} and
* {@link module:ui/view~View#t}.
*
* The strings may contain placeholders (`%<index>`) for values which are passed as the second argument.
* `<index>` is the index in the `values` array.
*
* editor.t( 'Created file "%0" in %1ms.', [ fileName, timeTaken ] );
*
* This method's context is statically bound to Locale instance,
* so it can be called as a function:
*
* const t = this.t;
* t( 'Label' );
*
* @method #t
* @param {String} str The string to translate.
* @param {String[]} values Values that should be used to interpolate the string.
*/
this.t = ( ...args ) => this._t( ...args );
}
/**
* Base for the {@link #t} method.
*
* @private
*/
_t( str, values ) {
let translatedString = translate( this.lang, str );
if ( values ) {
translatedString = translatedString.replace( /%(\d+)/g, ( match, index ) => {
return ( index < values.length ) ? values[ index ] : match;
} );
}
return translatedString;
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/mix.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module utils/mix
*/
/**
* Copies enumerable properties and symbols from the objects given as 2nd+ parameters to the
* prototype of first object (a constructor).
*
* class Editor {
* ...
* }
*
* const SomeMixin = {
* a() {
* return 'a';
* }
* };
*
* mix( Editor, SomeMixin, ... );
*
* new Editor().a(); // -> 'a'
*
* Note: Properties which already exist in the base class will not be overriden.
*
* @param {Function} [baseClass] Class which prototype will be extended.
* @param {Object} [...mixins] Objects from which to get properties.
*/
function mix( baseClass, ...mixins ) {
mixins.forEach( mixin => {
Object.getOwnPropertyNames( mixin ).concat( Object.getOwnPropertySymbols( mixin ) )
.forEach( key => {
if ( key in baseClass.prototype ) {
return;
}
const sourceDescriptor = Object.getOwnPropertyDescriptor( mixin, key );
sourceDescriptor.enumerable = false;
Object.defineProperty( baseClass.prototype, key, sourceDescriptor );
} );
} );
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/spy.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module utils/spy
*/
/**
* Creates a spy function (ala Sinon.js) that can be used to inspect call to it.
*
* The following are the present features:
*
* * spy.called: property set to `true` if the function has been called at least once.
*
* @returns {Function} The spy function.
*/
function spy() {
return function spy() {
spy.called = true;
};
}
/* harmony default export */ var src_spy = (spy);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/eventinfo.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module utils/eventinfo
*/
/**
* The event object passed to event callbacks. It is used to provide information about the event as well as a tool to
* manipulate it.
*/
class eventinfo_EventInfo {
/**
* @param {Object} source The emitter.
* @param {String} name The event name.
*/
constructor( source, name ) {
/**
* The object that fired the event.
*
* @readonly
* @member {Object}
*/
this.source = source;
/**
* The event name.
*
* @readonly
* @member {String}
*/
this.name = name;
/**
* Path this event has followed. See {@link module:utils/emittermixin~EmitterMixin#delegate}.
*
* @readonly
* @member {Array.<Object>}
*/
this.path = [];
// The following methods are defined in the constructor because they must be re-created per instance.
/**
* Stops the event emitter to call further callbacks for this event interaction.
*
* @method #stop
*/
this.stop = src_spy();
/**
* Removes the current callback from future interactions of this event.
*
* @method #off
*/
this.off = src_spy();
/**
* The value which will be returned by {@link module:utils/emittermixin~EmitterMixin#fire}.
*
* It's `undefined` by default and can be changed by an event listener:
*
* dataController.fire( 'getSelectedContent', ( evt ) => {
* // This listener will make `dataController.fire( 'getSelectedContent' )`
* // always return an empty DocumentFragment.
* evt.return = new DocumentFragment();
*
* // Make sure no other listeners are executed.
* evt.stop();
* } );
*
* @member #return
*/
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/uid.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module utils/uid
*/
/**
* Returns a unique id. This id is a number (starting from 1) which will never get repeated on successive calls
* to this method.
*
* @returns {String} A number representing the id.
*/
function uid() {
let uuid = 'e'; // Make sure that id does not start with number.
for ( let i = 0; i < 8; i++ ) {
uuid += Math.floor( ( 1 + Math.random() ) * 0x10000 ).toString( 16 ).substring( 1 );
}
return uuid;
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/priorities.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module utils/priorities
*/
/**
* String representing a priority value.
*
* @typedef {'highest'|'high'|'normal'|'low'|'lowest'} module:utils/priorities~PriorityString
*/
/**
* Provides group of constants to use instead of hardcoding numeric priority values.
*
* @namespace
*/
const priorities = {
/**
* Converts a string with priority name to it's numeric value. If `Number` is given, it just returns it.
*
* @static
* @param {module:utils/priorities~PriorityString|Number} priority Priority to convert.
* @returns {Number} Converted priority.
*/
get( priority ) {
if ( typeof priority != 'number' ) {
return this[ priority ] || this.normal;
} else {
return priority;
}
},
highest: 100000,
high: 1000,
normal: 0,
low: -1000,
lowest: -100000
};
/* harmony default export */ var src_priorities = (priorities);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/emittermixin.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module utils/emittermixin
*/
const _listeningTo = Symbol( 'listeningTo' );
const _emitterId = Symbol( 'emitterId' );
/**
* Mixin that injects the events API into its host.
*
* @mixin EmitterMixin
* @implements module:utils/emittermixin~Emitter
*/
const EmitterMixin = {
/**
* Registers a callback function to be executed when an event is fired.
*
* Events can be grouped in namespaces using `:`.
* When namespaced event is fired, it additionally fires all callbacks for that namespace.
*
* myEmitter.on( 'myGroup', genericCallback );
* myEmitter.on( 'myGroup:myEvent', specificCallback );
*
* // genericCallback is fired.
* myEmitter.fire( 'myGroup' );
* // both genericCallback and specificCallback are fired.
* myEmitter.fire( 'myGroup:myEvent' );
* // genericCallback is fired even though there are no callbacks for "foo".
* myEmitter.fire( 'myGroup:foo' );
*
* An event callback can {@link module:utils/eventinfo~EventInfo#stop stop the event} and
* set the {@link module:utils/eventinfo~EventInfo#return return value} of the {@link #fire} method.
*
* @method #on
* @param {String} event The name of the event.
* @param {Function} callback The function to be called on event.
* @param {Object} [options={}] Additional options.
* @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
* the priority value the sooner the callback will be fired. Events having the same priority are called in the
* order they were added.
*/
on( event, callback, options = {} ) {
createEventNamespace( this, event );
const lists = getCallbacksListsForNamespace( this, event );
const priority = src_priorities.get( options.priority );
callback = {
callback,
priority
};
// Add the callback to all callbacks list.
for ( const callbacks of lists ) {
// Add the callback to the list in the right priority position.
let added = false;
for ( let i = 0; i < callbacks.length; i++ ) {
if ( callbacks[ i ].priority < priority ) {
callbacks.splice( i, 0, callback );
added = true;
break;
}
}
// Add at the end, if right place was not found.
if ( !added ) {
callbacks.push( callback );
}
}
},
/**
* Registers a callback function to be executed on the next time the event is fired only. This is similar to
* calling {@link #on} followed by {@link #off} in the callback.
*
* @method #once
* @param {String} event The name of the event.
* @param {Function} callback The function to be called on event.
* @param {Object} [options={}] Additional options.
* @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
* the priority value the sooner the callback will be fired. Events having the same priority are called in the
* order they were added.
*/
once( event, callback, options ) {
const onceCallback = function( event, ...args ) {
// Go off() at the first call.
event.off();
// Go with the original callback.
callback.call( this, event, ...args );
};
// Make a similar on() call, simply replacing the callback.
this.on( event, onceCallback, options );
},
/**
* Stops executing the callback on the given event.
*
* @method #off
* @param {String} event The name of the event.
* @param {Function} callback The function to stop being called.
*/
off( event, callback ) {
const lists = getCallbacksListsForNamespace( this, event );
for ( const callbacks of lists ) {
for ( let i = 0; i < callbacks.length; i++ ) {
if ( callbacks[ i ].callback == callback ) {
// Remove the callback from the list (fixing the next index).
callbacks.splice( i, 1 );
i--;
}
}
}
},
/**
* Registers a callback function to be executed when an event is fired in a specific (emitter) object.
*
* @method #listenTo
* @param {module:utils/emittermixin~Emitter} emitter The object that fires the event.
* @param {String} event The name of the event.
* @param {Function} callback The function to be called on event.
* @param {Object} [options={}] Additional options.
* @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
* the priority value the sooner the callback will be fired. Events having the same priority are called in the
* order they were added.
*/
listenTo( emitter, event, callback, options ) {
let emitterInfo, eventCallbacks;
// _listeningTo contains a list of emitters that this object is listening to.
// This list has the following format:
//
// _listeningTo: {
// emitterId: {
// emitter: emitter,
// callbacks: {
// event1: [ callback1, callback2, ... ]
// ....
// }
// },
// ...
// }
if ( !this[ _listeningTo ] ) {
this[ _listeningTo ] = {};
}
const emitters = this[ _listeningTo ];
if ( !_getEmitterId( emitter ) ) {
_setEmitterId( emitter );
}
const emitterId = _getEmitterId( emitter );
if ( !( emitterInfo = emitters[ emitterId ] ) ) {
emitterInfo = emitters[ emitterId ] = {
emitter,
callbacks: {}
};
}
if ( !( eventCallbacks = emitterInfo.callbacks[ event ] ) ) {
eventCallbacks = emitterInfo.callbacks[ event ] = [];
}
eventCallbacks.push( callback );
// Finally register the callback to the event.
emitter.on( event, callback, options );
},
/**
* Stops listening for events. It can be used at different levels:
*
* * To stop listening to a specific callback.
* * To stop listening to a specific event.
* * To stop listening to all events fired by a specific object.
* * To stop listening to all events fired by all object.
*
* @method #stopListening
* @param {module:utils/emittermixin~Emitter} [emitter] The object to stop listening to. If omitted, stops it for all objects.
* @param {String} [event] (Requires the `emitter`) The name of the event to stop listening to. If omitted, stops it
* for all events from `emitter`.
* @param {Function} [callback] (Requires the `event`) The function to be removed from the call list for the given
* `event`.
*/
stopListening( emitter, event, callback ) {
const emitters = this[ _listeningTo ];
let emitterId = emitter && _getEmitterId( emitter );
const emitterInfo = emitters && emitterId && emitters[ emitterId ];
const eventCallbacks = emitterInfo && event && emitterInfo.callbacks[ event ];
// Stop if nothing has been listened.
if ( !emitters || ( emitter && !emitterInfo ) || ( event && !eventCallbacks ) ) {
return;
}
// All params provided. off() that single callback.
if ( callback ) {
emitter.off( event, callback );
}
// Only `emitter` and `event` provided. off() all callbacks for that event.
else if ( eventCallbacks ) {
while ( ( callback = eventCallbacks.pop() ) ) {
emitter.off( event, callback );
}
delete emitterInfo.callbacks[ event ];
}
// Only `emitter` provided. off() all events for that emitter.
else if ( emitterInfo ) {
for ( event in emitterInfo.callbacks ) {
this.stopListening( emitter, event );
}
delete emitters[ emitterId ];
}
// No params provided. off() all emitters.
else {
for ( emitterId in emitters ) {
this.stopListening( emitters[ emitterId ].emitter );
}
delete this[ _listeningTo ];
}
},
/**
* Fires an event, executing all callbacks registered for it.
*
* The first parameter passed to callbacks is an {@link module:utils/eventinfo~EventInfo} object,
* followed by the optional `args` provided in the `fire()` method call.
*
* @method #fire
* @param {String|module:utils/eventinfo~EventInfo} eventOrInfo The name of the event or `EventInfo` object if event is delegated.
* @param {...*} [args] Additional arguments to be passed to the callbacks.
* @returns {*} By default the method returns `undefined`. However, the return value can be changed by listeners
* through modification of the {@link module:utils/eventinfo~EventInfo#return}'s value (the event info
* is the first param of every callback).
*/
fire( eventOrInfo, ...args ) {
const eventInfo = eventOrInfo instanceof eventinfo_EventInfo ? eventOrInfo : new eventinfo_EventInfo( this, eventOrInfo );
const event = eventInfo.name;
let callbacks = getCallbacksForEvent( this, event );
// Record that the event passed this emitter on its path.
eventInfo.path.push( this );
// Handle event listener callbacks first.
if ( callbacks ) {
// Arguments passed to each callback.
const callbackArgs = [ eventInfo, ...args ];
// Copying callbacks array is the easiest and most secure way of preventing infinite loops, when event callbacks
// are added while processing other callbacks. Previous solution involved adding counters (unique ids) but
// failed if callbacks were added to the queue before currently processed callback.
// If this proves to be too inefficient, another method is to change `.on()` so callbacks are stored if same
// event is currently processed. Then, `.fire()` at the end, would have to add all stored events.
callbacks = Array.from( callbacks );
for ( let i = 0; i < callbacks.length; i++ ) {
callbacks[ i ].callback.apply( this, callbackArgs );
// Remove the callback from future requests if off() has been called.
if ( eventInfo.off.called ) {
// Remove the called mark for the next calls.
delete eventInfo.off.called;
this.off( event, callbacks[ i ].callback );
}
// Do not execute next callbacks if stop() was called.
if ( eventInfo.stop.called ) {
break;
}
}
}
// Delegate event to other emitters if needed.
if ( this._delegations ) {
const destinations = this._delegations.get( event );
const passAllDestinations = this._delegations.get( '*' );
if ( destinations ) {
fireDelegatedEvents( destinations, eventInfo, args );
}
if ( passAllDestinations ) {
fireDelegatedEvents( passAllDestinations, eventInfo, args );
}
}
return eventInfo.return;
},
/**
* Delegates selected events to another {@link module:utils/emittermixin~Emitter}. For instance:
*
* emitterA.delegate( 'eventX' ).to( emitterB );
* emitterA.delegate( 'eventX', 'eventY' ).to( emitterC );
*
* then `eventX` is delegated (fired by) `emitterB` and `emitterC` along with `data`:
*
* emitterA.fire( 'eventX', data );
*
* and `eventY` is delegated (fired by) `emitterC` along with `data`:
*
* emitterA.fire( 'eventY', data );
*
* @method #delegate
* @param {...String} events Event names that will be delegated to another emitter.
* @returns {module:utils/emittermixin~EmitterMixinDelegateChain}
*/
delegate( ...events ) {
return {
to: ( emitter, nameOrFunction ) => {
if ( !this._delegations ) {
this._delegations = new Map();
}
for ( const eventName of events ) {
const destinations = this._delegations.get( eventName );
if ( !destinations ) {
this._delegations.set( eventName, new Map( [ [ emitter, nameOrFunction ] ] ) );
} else {
destinations.set( emitter, nameOrFunction );
}
}
}
};
},
/**
* Stops delegating events. It can be used at different levels:
*
* * To stop delegating all events.
* * To stop delegating a specific event to all emitters.
* * To stop delegating a specific event to a specific emitter.
*
* @method #stopDelegating
* @param {String} [event] The name of the event to stop delegating. If omitted, stops it all delegations.
* @param {module:utils/emittermixin~Emitter} [emitter] (requires `event`) The object to stop delegating a particular event to.
* If omitted, stops delegation of `event` to all emitters.
*/
stopDelegating( event, emitter ) {
if ( !this._delegations ) {
return;
}
if ( !event ) {
this._delegations.clear();
} else if ( !emitter ) {
this._delegations.delete( event );
} else {
const destinations = this._delegations.get( event );
if ( destinations ) {
destinations.delete( emitter );
}
}
}
};
/* harmony default export */ var emittermixin = (EmitterMixin);
/**
* Checks if `listeningEmitter` listens to an emitter with given `listenedToEmitterId` and if so, returns that emitter.
* If not, returns `null`.
*
* @protected
* @param {module:utils/emittermixin~EmitterMixin} listeningEmitter Emitter that listens.
* @param {String} listenedToEmitterId Unique emitter id of emitter listened to.
* @returns {module:utils/emittermixin~EmitterMixin|null}
*/
function _getEmitterListenedTo( listeningEmitter, listenedToEmitterId ) {
if ( listeningEmitter[ _listeningTo ] && listeningEmitter[ _listeningTo ][ listenedToEmitterId ] ) {
return listeningEmitter[ _listeningTo ][ listenedToEmitterId ].emitter;
}
return null;
}
/**
* Sets emitter's unique id.
*
* **Note:** `_emitterId` can be set only once.
*
* @protected
* @param {module:utils/emittermixin~EmitterMixin} emitter Emitter for which id will be set.
* @param {String} [id] Unique id to set. If not passed, random unique id will be set.
*/
function _setEmitterId( emitter, id ) {
if ( !emitter[ _emitterId ] ) {
emitter[ _emitterId ] = id || uid();
}
}
/**
* Returns emitter's unique id.
*
* @protected
* @param {module:utils/emittermixin~EmitterMixin} emitter Emitter which id will be returned.
*/
function _getEmitterId( emitter ) {
return emitter[ _emitterId ];
}
// Gets the internal `_events` property of the given object.
// `_events` property store all lists with callbacks for registered event names.
// If there were no events registered on the object, empty `_events` object is created.
function getEvents( source ) {
if ( !source._events ) {
Object.defineProperty( source, '_events', {
value: {}
} );
}
return source._events;
}
// Creates event node for generic-specific events relation architecture.
function makeEventNode() {
return {
callbacks: [],
childEvents: []
};
}
// Creates an architecture for generic-specific events relation.
// If needed, creates all events for given eventName, i.e. if the first registered event
// is foo:bar:abc, it will create foo:bar:abc, foo:bar and foo event and tie them together.
// It also copies callbacks from more generic events to more specific events when
// specific events are created.
function createEventNamespace( source, eventName ) {
const events = getEvents( source );
// First, check if the event we want to add to the structure already exists.
if ( events[ eventName ] ) {
// If it exists, we don't have to do anything.
return;
}
// In other case, we have to create the structure for the event.
// Note, that we might need to create intermediate events too.
// I.e. if foo:bar:abc is being registered and we only have foo in the structure,
// we need to also register foo:bar.
// Currently processed event name.
let name = eventName;
// Name of the event that is a child event for currently processed event.
let childEventName = null;
// Array containing all newly created specific events.
const newEventNodes = [];
// While loop can't check for ':' index because we have to handle generic events too.
// In each loop, we truncate event name, going from the most specific name to the generic one.
// I.e. foo:bar:abc -> foo:bar -> foo.
while ( name !== '' ) {
if ( events[ name ] ) {
// If the currently processed event name is already registered, we can be sure
// that it already has all the structure created, so we can break the loop here
// as no more events need to be registered.
break;
}
// If this event is not yet registered, create a new object for it.
events[ name ] = makeEventNode();
// Add it to the array with newly created events.
newEventNodes.push( events[ name ] );
// Add previously processed event name as a child of this event.
if ( childEventName ) {
events[ name ].childEvents.push( childEventName );
}
childEventName = name;
// If `.lastIndexOf()` returns -1, `.substr()` will return '' which will break the loop.
name = name.substr( 0, name.lastIndexOf( ':' ) );
}
if ( name !== '' ) {
// If name is not empty, we found an already registered event that was a parent of the
// event we wanted to register.
// Copy that event's callbacks to newly registered events.
for ( const node of newEventNodes ) {
node.callbacks = events[ name ].callbacks.slice();
}
// Add last newly created event to the already registered event.
events[ name ].childEvents.push( childEventName );
}
}
// Gets an array containing callbacks list for a given event and it's more specific events.
// I.e. if given event is foo:bar and there is also foo:bar:abc event registered, this will
// return callback list of foo:bar and foo:bar:abc (but not foo).
// Returns empty array if given event has not been yet registered.
function getCallbacksListsForNamespace( source, eventName ) {
const eventNode = getEvents( source )[ eventName ];
if ( !eventNode ) {
return [];
}
let callbacksLists = [ eventNode.callbacks ];
for ( let i = 0; i < eventNode.childEvents.length; i++ ) {
const childCallbacksLists = getCallbacksListsForNamespace( source, eventNode.childEvents[ i ] );
callbacksLists = callbacksLists.concat( childCallbacksLists );
}
return callbacksLists;
}
// Get the list of callbacks for a given event, but only if there any callbacks have been registered.
// If there are no callbacks registered for given event, it checks if this is a specific event and looks
// for callbacks for it's more generic version.
function getCallbacksForEvent( source, eventName ) {
let event;
if ( !source._events || !( event = source._events[ eventName ] ) || !event.callbacks.length ) {
// There are no callbacks registered for specified eventName.
// But this could be a specific-type event that is in a namespace.
if ( eventName.indexOf( ':' ) > -1 ) {
// If the eventName is specific, try to find callback lists for more generic event.
return getCallbacksForEvent( source, eventName.substr( 0, eventName.lastIndexOf( ':' ) ) );
} else {
// If this is a top-level generic event, return null;
return null;
}
}
return event.callbacks;
}
// Fires delegated events for given map of destinations.
//
// @private
// * @param {Map.<utils.Emitter>} destinations A map containing `[ {@link utils.Emitter}, "event name" ]` pair destinations.
// * @param {utils.EventInfo} eventInfo The original event info object.
// * @param {Array.<*>} fireArgs Arguments the original event was fired with.
function fireDelegatedEvents( destinations, eventInfo, fireArgs ) {
for ( let [ emitter, name ] of destinations ) {
if ( !name ) {
name = eventInfo.name;
} else if ( typeof name == 'function' ) {
name = name( eventInfo.name );
}
const delegatedInfo = new eventinfo_EventInfo( eventInfo.source, name );
delegatedInfo.path = [ ...eventInfo.path ];
emitter.fire( delegatedInfo, ...fireArgs );
}
}
/**
* Interface representing classes which mix in {@link module:utils/emittermixin~EmitterMixin}.
*
* @interface Emitter
*/
/**
* The return value of {@link ~EmitterMixin#delegate}.
*
* @interface module:utils/emittermixin~EmitterMixinDelegateChain
*/
/**
* Selects destination for {@link module:utils/emittermixin~EmitterMixin#delegate} events.
*
* @method #to
* @param {module:utils/emittermixin~Emitter} emitter An `EmitterMixin` instance which is the destination for delegated events.
* @param {String|Function} nameOrFunction A custom event name or function which converts the original name string.
*/
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/eq.js
/**
* Performs a
* [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
* comparison between two values to determine if they are equivalent.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @returns {boolean} Returns `true` if the values are equivalent, else `false`.
* @example
*
* var object = { 'user': 'fred' };
* var other = { 'user': 'fred' };
*
* _.eq(object, object);
* // => true
*
* _.eq(object, other);
* // => false
*
* _.eq('a', 'a');
* // => true
*
* _.eq('a', Object('a'));
* // => false
*
* _.eq(NaN, NaN);
* // => true
*/
function eq(value, other) {
return value === other || (value !== value && other !== other);
}
/* harmony default export */ var lodash_eq = (eq);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_assignValue.js
/** Used for built-in method references. */
var _assignValue_objectProto = Object.prototype;
/** Used to check objects for own properties. */
var _assignValue_hasOwnProperty = _assignValue_objectProto.hasOwnProperty;
/**
* Assigns `value` to `key` of `object` if the existing value is not equivalent
* using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
* for equality comparisons.
*
* @private
* @param {Object} object The object to modify.
* @param {string} key The key of the property to assign.
* @param {*} value The value to assign.
*/
function assignValue(object, key, value) {
var objValue = object[key];
if (!(_assignValue_hasOwnProperty.call(object, key) && lodash_eq(objValue, value)) ||
(value === undefined && !(key in object))) {
object[key] = value;
}
}
/* harmony default export */ var _assignValue = (assignValue);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_copyObject.js
/**
* Copies properties of `source` to `object`.
*
* @private
* @param {Object} source The object to copy properties from.
* @param {Array} props The property identifiers to copy.
* @param {Object} [object={}] The object to copy properties to.
* @param {Function} [customizer] The function to customize copied values.
* @returns {Object} Returns `object`.
*/
function copyObject(source, props, object, customizer) {
object || (object = {});
var index = -1,
length = props.length;
while (++index < length) {
var key = props[index];
var newValue = customizer
? customizer(object[key], source[key], key, object, source)
: source[key];
_assignValue(object, key, newValue);
}
return object;
}
/* harmony default export */ var _copyObject = (copyObject);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseProperty.js
/**
* The base implementation of `_.property` without support for deep paths.
*
* @private
* @param {string} key The key of the property to get.
* @returns {Function} Returns the new accessor function.
*/
function baseProperty(key) {
return function(object) {
return object == null ? undefined : object[key];
};
}
/* harmony default export */ var _baseProperty = (baseProperty);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_getLength.js
/**
* Gets the "length" property value of `object`.
*
* **Note:** This function is used to avoid a
* [JIT bug](https://bugs.webkit.org/show_bug.cgi?id=142792) that affects
* Safari on at least iOS 8.1-8.3 ARM64.
*
* @private
* @param {Object} object The object to query.
* @returns {*} Returns the "length" value.
*/
var getLength = _baseProperty('length');
/* harmony default export */ var _getLength = (getLength);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isObject.js
/**
* Checks if `value` is the
* [language type](http://www.ecma-international.org/ecma-262/6.0/#sec-ecmascript-language-types)
* of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an object, else `false`.
* @example
*
* _.isObject({});
* // => true
*
* _.isObject([1, 2, 3]);
* // => true
*
* _.isObject(_.noop);
* // => true
*
* _.isObject(null);
* // => false
*/
function isObject(value) {
var type = typeof value;
return !!value && (type == 'object' || type == 'function');
}
/* harmony default export */ var lodash_isObject = (isObject);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isFunction.js
/** `Object#toString` result references. */
var funcTag = '[object Function]',
genTag = '[object GeneratorFunction]';
/** Used for built-in method references. */
var isFunction_objectProto = Object.prototype;
/**
* Used to resolve the
* [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring)
* of values.
*/
var isFunction_objectToString = isFunction_objectProto.toString;
/**
* Checks if `value` is classified as a `Function` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is correctly classified,
* else `false`.
* @example
*
* _.isFunction(_);
* // => true
*
* _.isFunction(/abc/);
* // => false
*/
function isFunction(value) {
// The use of `Object#toString` avoids issues with the `typeof` operator
// in Safari 8 which returns 'object' for typed array and weak map constructors,
// and PhantomJS 1.9 which returns 'function' for `NodeList` instances.
var tag = lodash_isObject(value) ? isFunction_objectToString.call(value) : '';
return tag == funcTag || tag == genTag;
}
/* harmony default export */ var lodash_isFunction = (isFunction);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isLength.js
/** Used as references for various `Number` constants. */
var MAX_SAFE_INTEGER = 9007199254740991;
/**
* Checks if `value` is a valid array-like length.
*
* **Note:** This function is loosely based on
* [`ToLength`](http://ecma-international.org/ecma-262/6.0/#sec-tolength).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a valid length,
* else `false`.
* @example
*
* _.isLength(3);
* // => true
*
* _.isLength(Number.MIN_VALUE);
* // => false
*
* _.isLength(Infinity);
* // => false
*
* _.isLength('3');
* // => false
*/
function isLength(value) {
return typeof value == 'number' &&
value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
}
/* harmony default export */ var lodash_isLength = (isLength);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isArrayLike.js
/**
* Checks if `value` is array-like. A value is considered array-like if it's
* not a function and has a `value.length` that's an integer greater than or
* equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is array-like, else `false`.
* @example
*
* _.isArrayLike([1, 2, 3]);
* // => true
*
* _.isArrayLike(document.body.children);
* // => true
*
* _.isArrayLike('abc');
* // => true
*
* _.isArrayLike(_.noop);
* // => false
*/
function isArrayLike(value) {
return value != null && lodash_isLength(_getLength(value)) && !lodash_isFunction(value);
}
/* harmony default export */ var lodash_isArrayLike = (isArrayLike);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_isIndex.js
/** Used as references for various `Number` constants. */
var _isIndex_MAX_SAFE_INTEGER = 9007199254740991;
/** Used to detect unsigned integer values. */
var reIsUint = /^(?:0|[1-9]\d*)$/;
/**
* Checks if `value` is a valid array-like index.
*
* @private
* @param {*} value The value to check.
* @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index.
* @returns {boolean} Returns `true` if `value` is a valid index, else `false`.
*/
function isIndex(value, length) {
length = length == null ? _isIndex_MAX_SAFE_INTEGER : length;
return !!length &&
(typeof value == 'number' || reIsUint.test(value)) &&
(value > -1 && value % 1 == 0 && value < length);
}
/* harmony default export */ var _isIndex = (isIndex);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_isIterateeCall.js
/**
* Checks if the given arguments are from an iteratee call.
*
* @private
* @param {*} value The potential iteratee value argument.
* @param {*} index The potential iteratee index or key argument.
* @param {*} object The potential iteratee object argument.
* @returns {boolean} Returns `true` if the arguments are from an iteratee call,
* else `false`.
*/
function isIterateeCall(value, index, object) {
if (!lodash_isObject(object)) {
return false;
}
var type = typeof index;
if (type == 'number'
? (lodash_isArrayLike(object) && _isIndex(index, object.length))
: (type == 'string' && index in object)
) {
return lodash_eq(object[index], value);
}
return false;
}
/* harmony default export */ var _isIterateeCall = (isIterateeCall);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_apply.js
/**
* A faster alternative to `Function#apply`, this function invokes `func`
* with the `this` binding of `thisArg` and the arguments of `args`.
*
* @private
* @param {Function} func The function to invoke.
* @param {*} thisArg The `this` binding of `func`.
* @param {Array} args The arguments to invoke `func` with.
* @returns {*} Returns the result of `func`.
*/
function apply(func, thisArg, args) {
var length = args.length;
switch (length) {
case 0: return func.call(thisArg);
case 1: return func.call(thisArg, args[0]);
case 2: return func.call(thisArg, args[0], args[1]);
case 3: return func.call(thisArg, args[0], args[1], args[2]);
}
return func.apply(thisArg, args);
}
/* harmony default export */ var _apply = (apply);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isSymbol.js
/** `Object#toString` result references. */
var symbolTag = '[object Symbol]';
/** Used for built-in method references. */
var isSymbol_objectProto = Object.prototype;
/**
* Used to resolve the
* [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring)
* of values.
*/
var isSymbol_objectToString = isSymbol_objectProto.toString;
/**
* Checks if `value` is classified as a `Symbol` primitive or object.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is correctly classified,
* else `false`.
* @example
*
* _.isSymbol(Symbol.iterator);
* // => true
*
* _.isSymbol('abc');
* // => false
*/
function isSymbol(value) {
return typeof value == 'symbol' ||
(lodash_isObjectLike(value) && isSymbol_objectToString.call(value) == symbolTag);
}
/* harmony default export */ var lodash_isSymbol = (isSymbol);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/toNumber.js
/** Used as references for various `Number` constants. */
var NAN = 0 / 0;
/** Used to match leading and trailing whitespace. */
var reTrim = /^\s+|\s+$/g;
/** Used to detect bad signed hexadecimal string values. */
var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;
/** Used to detect binary string values. */
var reIsBinary = /^0b[01]+$/i;
/** Used to detect octal string values. */
var reIsOctal = /^0o[0-7]+$/i;
/** Built-in method references without a dependency on `root`. */
var freeParseInt = parseInt;
/**
* Converts `value` to a number.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to process.
* @returns {number} Returns the number.
* @example
*
* _.toNumber(3.2);
* // => 3.2
*
* _.toNumber(Number.MIN_VALUE);
* // => 5e-324
*
* _.toNumber(Infinity);
* // => Infinity
*
* _.toNumber('3.2');
* // => 3.2
*/
function toNumber(value) {
if (typeof value == 'number') {
return value;
}
if (lodash_isSymbol(value)) {
return NAN;
}
if (lodash_isObject(value)) {
var other = lodash_isFunction(value.valueOf) ? value.valueOf() : value;
value = lodash_isObject(other) ? (other + '') : other;
}
if (typeof value != 'string') {
return value === 0 ? value : +value;
}
value = value.replace(reTrim, '');
var isBinary = reIsBinary.test(value);
return (isBinary || reIsOctal.test(value))
? freeParseInt(value.slice(2), isBinary ? 2 : 8)
: (reIsBadHex.test(value) ? NAN : +value);
}
/* harmony default export */ var lodash_toNumber = (toNumber);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/toFinite.js
/** Used as references for various `Number` constants. */
var INFINITY = 1 / 0,
MAX_INTEGER = 1.7976931348623157e+308;
/**
* Converts `value` to a finite number.
*
* @static
* @memberOf _
* @since 4.12.0
* @category Lang
* @param {*} value The value to convert.
* @returns {number} Returns the converted number.
* @example
*
* _.toFinite(3.2);
* // => 3.2
*
* _.toFinite(Number.MIN_VALUE);
* // => 5e-324
*
* _.toFinite(Infinity);
* // => 1.7976931348623157e+308
*
* _.toFinite('3.2');
* // => 3.2
*/
function toFinite(value) {
if (!value) {
return value === 0 ? value : 0;
}
value = lodash_toNumber(value);
if (value === INFINITY || value === -INFINITY) {
var sign = (value < 0 ? -1 : 1);
return sign * MAX_INTEGER;
}
return value === value ? value : 0;
}
/* harmony default export */ var lodash_toFinite = (toFinite);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/toInteger.js
/**
* Converts `value` to an integer.
*
* **Note:** This function is loosely based on
* [`ToInteger`](http://www.ecma-international.org/ecma-262/6.0/#sec-tointeger).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to convert.
* @returns {number} Returns the converted integer.
* @example
*
* _.toInteger(3.2);
* // => 3
*
* _.toInteger(Number.MIN_VALUE);
* // => 0
*
* _.toInteger(Infinity);
* // => 1.7976931348623157e+308
*
* _.toInteger('3.2');
* // => 3
*/
function toInteger(value) {
var result = lodash_toFinite(value),
remainder = result % 1;
return result === result ? (remainder ? result - remainder : result) : 0;
}
/* harmony default export */ var lodash_toInteger = (toInteger);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/rest.js
/** Used as the `TypeError` message for "Functions" methods. */
var FUNC_ERROR_TEXT = 'Expected a function';
/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeMax = Math.max;
/**
* Creates a function that invokes `func` with the `this` binding of the
* created function and arguments from `start` and beyond provided as
* an array.
*
* **Note:** This method is based on the
* [rest parameter](https://mdn.io/rest_parameters).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Function
* @param {Function} func The function to apply a rest parameter to.
* @param {number} [start=func.length-1] The start position of the rest parameter.
* @returns {Function} Returns the new function.
* @example
*
* var say = _.rest(function(what, names) {
* return what + ' ' + _.initial(names).join(', ') +
* (_.size(names) > 1 ? ', & ' : '') + _.last(names);
* });
*
* say('hello', 'fred', 'barney', 'pebbles');
* // => 'hello fred, barney, & pebbles'
*/
function rest(func, start) {
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
start = nativeMax(start === undefined ? (func.length - 1) : lodash_toInteger(start), 0);
return function() {
var args = arguments,
index = -1,
length = nativeMax(args.length - start, 0),
array = Array(length);
while (++index < length) {
array[index] = args[start + index];
}
switch (start) {
case 0: return func.call(this, array);
case 1: return func.call(this, args[0], array);
case 2: return func.call(this, args[0], args[1], array);
}
var otherArgs = Array(start + 1);
index = -1;
while (++index < start) {
otherArgs[index] = args[index];
}
otherArgs[start] = array;
return _apply(func, this, otherArgs);
};
}
/* harmony default export */ var lodash_rest = (rest);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_createAssigner.js
/**
* Creates a function like `_.assign`.
*
* @private
* @param {Function} assigner The function to assign values.
* @returns {Function} Returns the new assigner function.
*/
function createAssigner(assigner) {
return lodash_rest(function(object, sources) {
var index = -1,
length = sources.length,
customizer = length > 1 ? sources[length - 1] : undefined,
guard = length > 2 ? sources[2] : undefined;
customizer = (assigner.length > 3 && typeof customizer == 'function')
? (length--, customizer)
: undefined;
if (guard && _isIterateeCall(sources[0], sources[1], guard)) {
customizer = length < 3 ? undefined : customizer;
length = 1;
}
object = Object(object);
while (++index < length) {
var source = sources[index];
if (source) {
assigner(object, source, index, customizer);
}
}
return object;
});
}
/* harmony default export */ var _createAssigner = (createAssigner);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_isPrototype.js
/** Used for built-in method references. */
var _isPrototype_objectProto = Object.prototype;
/**
* Checks if `value` is likely a prototype object.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a prototype, else `false`.
*/
function isPrototype(value) {
var Ctor = value && value.constructor,
proto = (typeof Ctor == 'function' && Ctor.prototype) || _isPrototype_objectProto;
return value === proto;
}
/* harmony default export */ var _isPrototype = (isPrototype);
// EXTERNAL MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_root.js
var _root = __webpack_require__(2);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_Reflect.js
/** Built-in value references. */
var Reflect = _root["a" /* default */].Reflect;
/* harmony default export */ var _Reflect = (Reflect);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_iteratorToArray.js
/**
* Converts `iterator` to an array.
*
* @private
* @param {Object} iterator The iterator to convert.
* @returns {Array} Returns the converted array.
*/
function iteratorToArray(iterator) {
var data,
result = [];
while (!(data = iterator.next()).done) {
result.push(data.value);
}
return result;
}
/* harmony default export */ var _iteratorToArray = (iteratorToArray);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseKeysIn.js
/** Used for built-in method references. */
var _baseKeysIn_objectProto = Object.prototype;
/** Built-in value references. */
var enumerate = _Reflect ? _Reflect.enumerate : undefined,
propertyIsEnumerable = _baseKeysIn_objectProto.propertyIsEnumerable;
/**
* The base implementation of `_.keysIn` which doesn't skip the constructor
* property of prototypes or treat sparse arrays as dense.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names.
*/
function baseKeysIn(object) {
object = object == null ? object : Object(object);
var result = [];
for (var key in object) {
result.push(key);
}
return result;
}
// Fallback for IE < 9 with es6-shim.
if (enumerate && !propertyIsEnumerable.call({ 'valueOf': 1 }, 'valueOf')) {
baseKeysIn = function(object) {
return _iteratorToArray(enumerate(object));
};
}
/* harmony default export */ var _baseKeysIn = (baseKeysIn);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseTimes.js
/**
* The base implementation of `_.times` without support for iteratee shorthands
* or max array length checks.
*
* @private
* @param {number} n The number of times to invoke `iteratee`.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Array} Returns the array of results.
*/
function baseTimes(n, iteratee) {
var index = -1,
result = Array(n);
while (++index < n) {
result[index] = iteratee(index);
}
return result;
}
/* harmony default export */ var _baseTimes = (baseTimes);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isArrayLikeObject.js
/**
* This method is like `_.isArrayLike` except that it also checks if `value`
* is an object.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an array-like object,
* else `false`.
* @example
*
* _.isArrayLikeObject([1, 2, 3]);
* // => true
*
* _.isArrayLikeObject(document.body.children);
* // => true
*
* _.isArrayLikeObject('abc');
* // => false
*
* _.isArrayLikeObject(_.noop);
* // => false
*/
function isArrayLikeObject(value) {
return lodash_isObjectLike(value) && lodash_isArrayLike(value);
}
/* harmony default export */ var lodash_isArrayLikeObject = (isArrayLikeObject);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isArguments.js
/** `Object#toString` result references. */
var argsTag = '[object Arguments]';
/** Used for built-in method references. */
var isArguments_objectProto = Object.prototype;
/** Used to check objects for own properties. */
var isArguments_hasOwnProperty = isArguments_objectProto.hasOwnProperty;
/**
* Used to resolve the
* [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring)
* of values.
*/
var isArguments_objectToString = isArguments_objectProto.toString;
/** Built-in value references. */
var isArguments_propertyIsEnumerable = isArguments_objectProto.propertyIsEnumerable;
/**
* Checks if `value` is likely an `arguments` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is correctly classified,
* else `false`.
* @example
*
* _.isArguments(function() { return arguments; }());
* // => true
*
* _.isArguments([1, 2, 3]);
* // => false
*/
function isArguments(value) {
// Safari 8.1 incorrectly makes `arguments.callee` enumerable in strict mode.
return lodash_isArrayLikeObject(value) && isArguments_hasOwnProperty.call(value, 'callee') &&
(!isArguments_propertyIsEnumerable.call(value, 'callee') || isArguments_objectToString.call(value) == argsTag);
}
/* harmony default export */ var lodash_isArguments = (isArguments);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isArray.js
/**
* Checks if `value` is classified as an `Array` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @type {Function}
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is correctly classified,
* else `false`.
* @example
*
* _.isArray([1, 2, 3]);
* // => true
*
* _.isArray(document.body.children);
* // => false
*
* _.isArray('abc');
* // => false
*
* _.isArray(_.noop);
* // => false
*/
var isArray = Array.isArray;
/* harmony default export */ var lodash_isArray = (isArray);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isString.js
/** `Object#toString` result references. */
var stringTag = '[object String]';
/** Used for built-in method references. */
var isString_objectProto = Object.prototype;
/**
* Used to resolve the
* [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring)
* of values.
*/
var isString_objectToString = isString_objectProto.toString;
/**
* Checks if `value` is classified as a `String` primitive or object.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is correctly classified,
* else `false`.
* @example
*
* _.isString('abc');
* // => true
*
* _.isString(1);
* // => false
*/
function isString(value) {
return typeof value == 'string' ||
(!lodash_isArray(value) && lodash_isObjectLike(value) && isString_objectToString.call(value) == stringTag);
}
/* harmony default export */ var lodash_isString = (isString);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_indexKeys.js
/**
* Creates an array of index keys for `object` values of arrays,
* `arguments` objects, and strings, otherwise `null` is returned.
*
* @private
* @param {Object} object The object to query.
* @returns {Array|null} Returns index keys, else `null`.
*/
function indexKeys(object) {
var length = object ? object.length : undefined;
if (lodash_isLength(length) &&
(lodash_isArray(object) || lodash_isString(object) || lodash_isArguments(object))) {
return _baseTimes(length, String);
}
return null;
}
/* harmony default export */ var _indexKeys = (indexKeys);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/keysIn.js
/** Used for built-in method references. */
var keysIn_objectProto = Object.prototype;
/** Used to check objects for own properties. */
var keysIn_hasOwnProperty = keysIn_objectProto.hasOwnProperty;
/**
* Creates an array of the own and inherited enumerable property names of `object`.
*
* **Note:** Non-object values are coerced to objects.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Object
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names.
* @example
*
* function Foo() {
* this.a = 1;
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.keysIn(new Foo);
* // => ['a', 'b', 'c'] (iteration order is not guaranteed)
*/
function keysIn(object) {
var index = -1,
isProto = _isPrototype(object),
props = _baseKeysIn(object),
propsLength = props.length,
indexes = _indexKeys(object),
skipIndexes = !!indexes,
result = indexes || [],
length = result.length;
while (++index < propsLength) {
var key = props[index];
if (!(skipIndexes && (key == 'length' || _isIndex(key, length))) &&
!(key == 'constructor' && (isProto || !keysIn_hasOwnProperty.call(object, key)))) {
result.push(key);
}
}
return result;
}
/* harmony default export */ var lodash_keysIn = (keysIn);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/assignIn.js
/** Used for built-in method references. */
var assignIn_objectProto = Object.prototype;
/** Built-in value references. */
var assignIn_propertyIsEnumerable = assignIn_objectProto.propertyIsEnumerable;
/** Detect if properties shadowing those on `Object.prototype` are non-enumerable. */
var nonEnumShadows = !assignIn_propertyIsEnumerable.call({ 'valueOf': 1 }, 'valueOf');
/**
* This method is like `_.assign` except that it iterates over own and
* inherited source properties.
*
* **Note:** This method mutates `object`.
*
* @static
* @memberOf _
* @since 4.0.0
* @alias extend
* @category Object
* @param {Object} object The destination object.
* @param {...Object} [sources] The source objects.
* @returns {Object} Returns `object`.
* @see _.assign
* @example
*
* function Foo() {
* this.b = 2;
* }
*
* function Bar() {
* this.d = 4;
* }
*
* Foo.prototype.c = 3;
* Bar.prototype.e = 5;
*
* _.assignIn({ 'a': 1 }, new Foo, new Bar);
* // => { 'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5 }
*/
var assignIn = _createAssigner(function(object, source) {
if (nonEnumShadows || _isPrototype(source) || lodash_isArrayLike(source)) {
_copyObject(source, lodash_keysIn(source), object);
return;
}
for (var key in source) {
_assignValue(object, key, source[key]);
}
});
/* harmony default export */ var lodash_assignIn = (assignIn);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/extend.js
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/observablemixin.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module utils/observablemixin
*/
const attributesSymbol = Symbol( 'attributes' );
const boundObservablesSymbol = Symbol( 'boundObservables' );
const boundAttributesSymbol = Symbol( 'boundAttributes' );
/**
* Mixin that injects the "observable attributes" and data binding functionality.
* Used mainly in the {@link module:ui/model~Model} class.
*
* @mixin ObservableMixin
* @mixes module:utils/emittermixin~EmitterMixin
* @implements module:utils/observablemixin~Observable
*/
const ObservableMixin = {
/**
* Creates and sets the value of an observable attribute of this object. Such an attribute becomes a part
* of the state and is be observable.
*
* It accepts also a single object literal containing key/value pairs with attributes to be set.
*
* This method throws the observable-set-cannot-override error if the observable instance already
* have a property with a given attribute name. This prevents from mistakenly overriding existing
* properties and methods, but means that `foo.set( 'bar', 1 )` may be slightly slower than `foo.bar = 1`.
*
* @method #set
* @param {String} name The attributes name.
* @param {*} value The attributes value.
*/
set( name, value ) {
// If the first parameter is an Object, iterate over its properties.
if ( lodash_isObject( name ) ) {
Object.keys( name ).forEach( attr => {
this.set( attr, name[ attr ] );
}, this );
return;
}
initObservable( this );
const attributes = this[ attributesSymbol ];
if ( ( name in this ) && !attributes.has( name ) ) {
/**
* Cannot override an existing property.
*
* This error is thrown when trying to {@link ~Observable#set set} an attribute with
* a name of an already existing property. For example:
*
* let observable = new Model();
* observable.property = 1;
* observable.set( 'property', 2 ); // throws
*
* observable.set( 'attr', 1 );
* observable.set( 'attr', 2 ); // ok, because this is an existing attribute.
*
* @error observable-set-cannot-override
*/
throw new CKEditorError( 'observable-set-cannot-override: Cannot override an existing property.' );
}
Object.defineProperty( this, name, {
enumerable: true,
configurable: true,
get() {
return attributes.get( name );
},
set( value ) {
const oldValue = attributes.get( name );
// Allow undefined as an initial value like A.define( 'x', undefined ) (#132).
// Note: When attributes map has no such own property, then its value is undefined.
if ( oldValue !== value || !attributes.has( name ) ) {
attributes.set( name, value );
this.fire( 'change:' + name, name, value, oldValue );
}
}
} );
this[ name ] = value;
},
/**
* Binds observable attributes to another objects implementing {@link ~ObservableMixin}
* interface (like {@link module:ui/model~Model}).
*
* Once bound, the observable will immediately share the current state of attributes
* of the observable it is bound to and react to the changes to these attributes
* in the future.
*
* **Note**: To release the binding use {@link module:utils/observablemixin~ObservableMixin#unbind}.
*
* A.bind( 'a' ).to( B );
* A.bind( 'a' ).to( B, 'b' );
* A.bind( 'a', 'b' ).to( B, 'c', 'd' );
* A.bind( 'a' ).to( B, 'b', C, 'd', ( b, d ) => b + d );
*
* @method #bind
* @param {...String} bindAttrs Observable attributes that will be bound to another observable(s).
* @returns {module:utils/observablemixin~BindChain}
*/
bind( ...bindAttrs ) {
if ( !bindAttrs.length || !isStringArray( bindAttrs ) ) {
/**
* All attributes must be strings.
*
* @error observable-bind-wrong-attrs
*/
throw new CKEditorError( 'observable-bind-wrong-attrs: All attributes must be strings.' );
}
if ( ( new Set( bindAttrs ) ).size !== bindAttrs.length ) {
/**
* Attributes must be unique.
*
* @error observable-bind-duplicate-attrs
*/
throw new CKEditorError( 'observable-bind-duplicate-attrs: Attributes must be unique.' );
}
initObservable( this );
const boundAttributes = this[ boundAttributesSymbol ];
bindAttrs.forEach( attrName => {
if ( boundAttributes.has( attrName ) ) {
/**
* Cannot bind the same attribute more that once.
*
* @error observable-bind-rebind
*/
throw new CKEditorError( 'observable-bind-rebind: Cannot bind the same attribute more that once.' );
}
} );
const bindings = new Map();
/**
* @typedef Binding
* @type Object
* @property {Array} attr Attribute which is bound.
* @property {Array} to Array of observable–attribute components of the binding (`{ observable: ..., attr: .. }`).
* @property {Array} callback A function which processes `to` components.
*/
bindAttrs.forEach( a => {
const binding = { attr: a, to: [] };
boundAttributes.set( a, binding );
bindings.set( a, binding );
} );
/**
* @typedef BindChain
* @type Object
* @property {Function} to See {@link ~ObservableMixin#_bindTo}.
* @property {module:utils/observablemixin~Observable} _observable The observable which initializes the binding.
* @property {Array} _bindAttrs Array of `_observable` attributes to be bound.
* @property {Array} _to Array of `to()` observable–attributes (`{ observable: toObservable, attrs: ...toAttrs }`).
* @property {Map} _bindings Stores bindings to be kept in
* {@link ~ObservableMixin#_boundAttributes}/{@link ~ObservableMixin#_boundObservables}
* initiated in this binding chain.
*/
return {
to: bindTo,
_observable: this,
_bindAttrs: bindAttrs,
_to: [],
_bindings: bindings
};
},
/**
* Removes the binding created with {@link ~ObservableMixin#bind}.
*
* A.unbind( 'a' );
* A.unbind();
*
* @method #unbind
* @param {...String} [unbindAttrs] Observable attributes to be unbound. All the bindings will
* be released if no attributes provided.
*/
unbind( ...unbindAttrs ) {
// Nothing to do here if not inited yet.
if ( !( attributesSymbol in this ) ) {
return;
}
const boundAttributes = this[ boundAttributesSymbol ];
const boundObservables = this[ boundObservablesSymbol ];
if ( unbindAttrs.length ) {
if ( !isStringArray( unbindAttrs ) ) {
/**
* Attributes must be strings.
*
* @error observable-unbind-wrong-attrs
*/
throw new CKEditorError( 'observable-unbind-wrong-attrs: Attributes must be strings.' );
}
unbindAttrs.forEach( attrName => {
const binding = boundAttributes.get( attrName );
let toObservable, toAttr, toAttrs, toAttrBindings;
binding.to.forEach( to => {
// TODO: ES6 destructuring.
toObservable = to[ 0 ];
toAttr = to[ 1 ];
toAttrs = boundObservables.get( toObservable );
toAttrBindings = toAttrs[ toAttr ];
toAttrBindings.delete( binding );
if ( !toAttrBindings.size ) {
delete toAttrs[ toAttr ];
}
if ( !Object.keys( toAttrs ).length ) {
boundObservables.delete( toObservable );
this.stopListening( toObservable, 'change' );
}
} );
boundAttributes.delete( attrName );
} );
} else {
boundObservables.forEach( ( bindings, boundObservable ) => {
this.stopListening( boundObservable, 'change' );
} );
boundObservables.clear();
boundAttributes.clear();
}
},
/**
* Turns the given methods of this object into event-based ones. This means that the new method will fire an event
* (named after the method) and the original action will be plugged as a listener to that event.
*
* This is a very simplified method decoration. Itself it doesn't change the behavior of a method (expect adding the event),
* but it allows to modify it later on by listening to the method's event.
*
* For example, in order to cancel the method execution one can stop the event:
*
* class Foo {
* constructor() {
* this.decorate( 'method' );
* }
*
* method() {
* console.log( 'called!' );
* }
* }
*
* const foo = new Foo();
* foo.on( 'method', ( evt ) => {
* evt.stop();
* }, { priority: 'high' } );
*
* foo.method(); // Nothing is logged.
*
*
* Note: we used a high priority listener here to execute this callback before the one which
* calls the orignal method (which used the default priority).
*
* It's also possible to change the return value:
*
* foo.on( 'method', ( evt ) => {
* evt.return = 'Foo!';
* } );
*
* foo.method(); // -> 'Foo'
*
* Finally, it's possible to access and modify the parameters:
*
* method( a, b ) {
* console.log( `${ a }, ${ b }` );
* }
*
* // ...
*
* foo.on( 'method', ( evt, args ) => {
* args[ 0 ] = 3;
*
* console.log( args[ 1 ] ); // -> 2
* }, { priority: 'high' } );
*
* foo.method( 1, 2 ); // -> '3, 2'
*
* @method #decorate
* @param {String} methodName Name of the method to decorate.
*/
decorate( methodName ) {
const originalMethod = this[ methodName ];
if ( !originalMethod ) {
/**
* Cannot decorate an undefined method.
*
* @error observablemixin-cannot-decorate-undefined
* @param {Object} object The object which method should be decorated.
* @param {String} methodName Name of the method which does not exist.
*/
throw new CKEditorError(
'observablemixin-cannot-decorate-undefined: Cannot decorate an undefined method.',
{ object: this, methodName }
);
}
this.on( methodName, ( evt, args ) => {
evt.return = originalMethod.apply( this, args );
} );
this[ methodName ] = function( ...args ) {
return this.fire( methodName, args );
};
}
/**
* @private
* @member ~ObservableMixin#_boundAttributes
*/
/**
* @private
* @member ~ObservableMixin#_boundObservables
*/
/**
* @private
* @member ~ObservableMixin#_bindTo
*/
};
/* harmony default export */ var observablemixin = (ObservableMixin);
// Init symbol properties needed to for the observable mechanism to work.
//
// @private
// @param {module:utils/observablemixin~ObservableMixin} observable
function initObservable( observable ) {
// Do nothing if already inited.
if ( attributesSymbol in observable ) {
return;
}
// The internal hash containing the observable's state.
//
// @private
// @type {Map}
Object.defineProperty( observable, attributesSymbol, {
value: new Map()
} );
// Map containing bindings to external observables. It shares the binding objects
// (`{ observable: A, attr: 'a', to: ... }`) with {@link module:utils/observablemixin~ObservableMixin#_boundAttributes} and
// it is used to observe external observables to update own attributes accordingly.
// See {@link module:utils/observablemixin~ObservableMixin#bind}.
//
// A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
// console.log( A._boundObservables );
//
// Map( {
// B: {
// x: Set( [
// { observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
// { observable: A, attr: 'c', to: [ [ B, 'x' ] ] }
// ] ),
// y: Set( [
// { observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
// ] )
// }
// } )
//
// A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
// console.log( A._boundObservables );
//
// Map( {
// B: {
// x: Set( [
// { observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
// { observable: A, attr: 'c', to: [ [ B, 'x' ] ] }
// ] ),
// y: Set( [
// { observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
// ] ),
// z: Set( [
// { observable: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
// ] )
// },
// C: {
// w: Set( [
// { observable: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
// ] )
// }
// } )
//
// @private
// @type {Map}
Object.defineProperty( observable, boundObservablesSymbol, {
value: new Map()
} );
// Object that stores which attributes of this observable are bound and how. It shares
// the binding objects (`{ observable: A, attr: 'a', to: ... }`) with {@link utils.ObservableMixin#_boundObservables}.
// This data structure is a reverse of {@link utils.ObservableMixin#_boundObservables} and it is helpful for
// {@link utils.ObservableMixin#unbind}.
//
// See {@link utils.ObservableMixin#bind}.
//
// A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
// console.log( A._boundAttributes );
//
// Map( {
// a: { observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
// b: { observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
// c: { observable: A, attr: 'c', to: [ [ B, 'x' ] ] }
// } )
//
// A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
// console.log( A._boundAttributes );
//
// Map( {
// a: { observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
// b: { observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
// c: { observable: A, attr: 'c', to: [ [ B, 'x' ] ] },
// d: { observable: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
// } )
//
// @private
// @type {Map}
Object.defineProperty( observable, boundAttributesSymbol, {
value: new Map()
} );
}
// A chaining for {@link module:utils/observablemixin~ObservableMixin#bind} providing `.to()` interface.
//
// @private
// @param {...[Observable|String|Function]} args Arguments of the `.to( args )` binding.
function bindTo( ...args ) {
const parsedArgs = parseBindToArgs( ...args );
const bindingsKeys = Array.from( this._bindings.keys() );
const numberOfBindings = bindingsKeys.length;
// Eliminate A.bind( 'x' ).to( B, C )
if ( !parsedArgs.callback && parsedArgs.to.length > 1 ) {
/**
* Binding multiple observables only possible with callback.
*
* @error observable-bind-no-callback
*/
throw new CKEditorError( 'observable-bind-to-no-callback: Binding multiple observables only possible with callback.' );
}
// Eliminate A.bind( 'x', 'y' ).to( B, callback )
if ( numberOfBindings > 1 && parsedArgs.callback ) {
/**
* Cannot bind multiple attributes and use a callback in one binding.
*
* @error observable-bind-to-extra-callback
*/
throw new CKEditorError( 'observable-bind-to-extra-callback: Cannot bind multiple attributes and use a callback in one binding.' );
}
parsedArgs.to.forEach( to => {
// Eliminate A.bind( 'x', 'y' ).to( B, 'a' )
if ( to.attrs.length && to.attrs.length !== numberOfBindings ) {
/**
* The number of attributes must match.
*
* @error observable-bind-to-attrs-length
*/
throw new CKEditorError( 'observable-bind-to-attrs-length: The number of attributes must match.' );
}
// When no to.attrs specified, observing source attributes instead i.e.
// A.bind( 'x', 'y' ).to( B ) -> Observe B.x and B.y
if ( !to.attrs.length ) {
to.attrs = this._bindAttrs;
}
} );
this._to = parsedArgs.to;
// Fill {@link BindChain#_bindings} with callback. When the callback is set there's only one binding.
if ( parsedArgs.callback ) {
this._bindings.get( bindingsKeys[ 0 ] ).callback = parsedArgs.callback;
}
attachBindToListeners( this._observable, this._to );
// Update observable._boundAttributes and observable._boundObservables.
updateBindToBound( this );
// Set initial values of bound attributes.
this._bindAttrs.forEach( attrName => {
updateBoundObservableAttr( this._observable, attrName );
} );
}
// Check if all entries of the array are of `String` type.
//
// @private
// @param {Array} arr An array to be checked.
// @returns {Boolean}
function isStringArray( arr ) {
return arr.every( a => typeof a == 'string' );
}
// Parses and validates {@link Observable#bind}`.to( args )` arguments and returns
// an object with a parsed structure. For example
//
// A.bind( 'x' ).to( B, 'a', C, 'b', call );
//
// becomes
//
// {
// to: [
// { observable: B, attrs: [ 'a' ] },
// { observable: C, attrs: [ 'b' ] },
// ],
// callback: call
// }
//
// @private
// @param {...*} args Arguments of {@link Observable#bind}`.to( args )`.
// @returns {Object}
function parseBindToArgs( ...args ) {
// Eliminate A.bind( 'x' ).to()
if ( !args.length ) {
/**
* Invalid argument syntax in `to()`.
*
* @error observable-bind-to-parse-error
*/
throw new CKEditorError( 'observable-bind-to-parse-error: Invalid argument syntax in `to()`.' );
}
const parsed = { to: [] };
let lastObservable;
if ( typeof args[ args.length - 1 ] == 'function' ) {
parsed.callback = args.pop();
}
args.forEach( a => {
if ( typeof a == 'string' ) {
lastObservable.attrs.push( a );
} else if ( typeof a == 'object' ) {
lastObservable = { observable: a, attrs: [] };
parsed.to.push( lastObservable );
} else {
throw new CKEditorError( 'observable-bind-to-parse-error: Invalid argument syntax in `to()`.' );
}
} );
return parsed;
}
// Synchronizes {@link module:utils/observablemixin#_boundObservables} with {@link Binding}.
//
// @private
// @param {Binding} binding A binding to store in {@link Observable#_boundObservables}.
// @param {Observable} toObservable A observable, which is a new component of `binding`.
// @param {String} toAttrName A name of `toObservable`'s attribute, a new component of the `binding`.
function updateBoundObservables( observable, binding, toObservable, toAttrName ) {
const boundObservables = observable[ boundObservablesSymbol ];
const bindingsToObservable = boundObservables.get( toObservable );
const bindings = bindingsToObservable || {};
if ( !bindings[ toAttrName ] ) {
bindings[ toAttrName ] = new Set();
}
// Pass the binding to a corresponding Set in `observable._boundObservables`.
bindings[ toAttrName ].add( binding );
if ( !bindingsToObservable ) {
boundObservables.set( toObservable, bindings );
}
}
// Synchronizes {@link Observable#_boundAttributes} and {@link Observable#_boundObservables}
// with {@link BindChain}.
//
// Assuming the following binding being created
//
// A.bind( 'a', 'b' ).to( B, 'x', 'y' );
//
// the following bindings were initialized by {@link Observable#bind} in {@link BindChain#_bindings}:
//
// {
// a: { observable: A, attr: 'a', to: [] },
// b: { observable: A, attr: 'b', to: [] },
// }
//
// Iterate over all bindings in this chain and fill their `to` properties with
// corresponding to( ... ) arguments (components of the binding), so
//
// {
// a: { observable: A, attr: 'a', to: [ B, 'x' ] },
// b: { observable: A, attr: 'b', to: [ B, 'y' ] },
// }
//
// Then update the structure of {@link Observable#_boundObservables} with updated
// binding, so it becomes:
//
// Map( {
// B: {
// x: Set( [
// { observable: A, attr: 'a', to: [ [ B, 'x' ] ] }
// ] ),
// y: Set( [
// { observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
// ] )
// }
// } )
//
// @private
// @param {BindChain} chain The binding initialized by {@link Observable#bind}.
function updateBindToBound( chain ) {
let toAttr;
chain._bindings.forEach( ( binding, attrName ) => {
// Note: For a binding without a callback, this will run only once
// like in A.bind( 'x', 'y' ).to( B, 'a', 'b' )
// TODO: ES6 destructuring.
chain._to.forEach( to => {
toAttr = to.attrs[ binding.callback ? 0 : chain._bindAttrs.indexOf( attrName ) ];
binding.to.push( [ to.observable, toAttr ] );
updateBoundObservables( chain._observable, binding, to.observable, toAttr );
} );
} );
}
// Updates an attribute of a {@link Observable} with a value
// determined by an entry in {@link Observable#_boundAttributes}.
//
// @private
// @param {Observable} observable A observable which attribute is to be updated.
// @param {String} attrName An attribute to be updated.
function updateBoundObservableAttr( observable, attrName ) {
const boundAttributes = observable[ boundAttributesSymbol ];
const binding = boundAttributes.get( attrName );
let attrValue;
// When a binding with callback is created like
//
// A.bind( 'a' ).to( B, 'b', C, 'c', callback );
//
// collect B.b and C.c, then pass them to callback to set A.a.
if ( binding.callback ) {
attrValue = binding.callback.apply( observable, binding.to.map( to => to[ 0 ][ to[ 1 ] ] ) );
} else {
attrValue = binding.to[ 0 ];
attrValue = attrValue[ 0 ][ attrValue[ 1 ] ];
}
if ( observable.hasOwnProperty( attrName ) ) {
observable[ attrName ] = attrValue;
} else {
observable.set( attrName, attrValue );
}
}
// Starts listening to changes in {@link BindChain._to} observables to update
// {@link BindChain._observable} {@link BindChain._bindAttrs}. Also sets the
// initial state of {@link BindChain._observable}.
//
// @private
// @param {BindChain} chain The chain initialized by {@link Observable#bind}.
function attachBindToListeners( observable, toBindings ) {
toBindings.forEach( to => {
const boundObservables = observable[ boundObservablesSymbol ];
let bindings;
// If there's already a chain between the observables (`observable` listens to
// `to.observable`), there's no need to create another `change` event listener.
if ( !boundObservables.get( to.observable ) ) {
observable.listenTo( to.observable, 'change', ( evt, attrName ) => {
bindings = boundObservables.get( to.observable )[ attrName ];
// Note: to.observable will fire for any attribute change, react
// to changes of attributes which are bound only.
if ( bindings ) {
bindings.forEach( binding => {
updateBoundObservableAttr( observable, binding.attr );
} );
}
} );
}
} );
}
lodash_assignIn( ObservableMixin, emittermixin );
/**
* Fired when an attribute changed value.
*
* observable.set( 'prop', 1 );
*
* observable.on( 'change:prop', ( evt, propertyName, newValue, oldValue ) => {
* console.log( `${ propertyName } has changed from ${ oldValue } to ${ newValue }` );
* } )
*
* observable.prop = 2; // -> 'prop has changed from 1 to 2'
*
* @event module:utils/observablemixin~ObservableMixin#change:{attribute}
* @param {String} name The attribute name.
* @param {*} value The new attribute value.
* @param {*} oldValue The previous attribute value.
*/
/**
* Interface representing classes which mix in {@link module:utils/observablemixin~ObservableMixin}.
*
* @interface Observable
*/
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/objecttomap.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module utils/objecttomap
*/
/**
* Transforms object to map.
*
* const map = objectToMap( { 'foo': 1, 'bar': 2 } );
* map.get( 'foo' ); // 1
*
* @param {Object} obj Object to transform.
* @returns {Map} Map created from object.
*/
function objectToMap( obj ) {
const map = new Map();
for ( const key in obj ) {
map.set( key, obj[ key ] );
}
return map;
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/tomap.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module utils/tomap
*/
/**
* Transforms object or iterable to map. Iterable needs to be in the format acceptable by the `Map` constructor.
*
* map = toMap( { 'foo': 1, 'bar': 2 } );
* map = toMap( [ [ 'foo', 1 ], [ 'bar', 2 ] ] );
* map = toMap( anotherMap );
*
* @param {Object|Iterable} data Object or iterable to transform.
* @returns {Map} Map created from data.
*/
function toMap( data ) {
if ( lodash_isPlainObject( data ) ) {
return objectToMap( data );
} else {
return new Map( data );
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/node.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/node
*/
/**
* Model node. Most basic structure of model tree.
*
* This is an abstract class that is a base for other classes representing different nodes in model.
*
* **Note:** If a node is detached from the model tree, you can manipulate it using it's API.
* However, it is **very important** that nodes already attached to model tree should be only changed through
* {@link module:engine/model/document~Document#batch Batch API}.
*
* Changes done by `Node` methods, like {@link module:engine/model/element~Element#insertChildren insertChildren} or
* {@link module:engine/model/node~Node#setAttribute setAttribute}
* do not generate {@link module:engine/model/operation/operation~Operation operations}
* which are essential for correct editor work if you modify nodes in {@link module:engine/model/document~Document document} root.
*
* The flow of working on `Node` (and classes that inherits from it) is as such:
* 1. You can create a `Node` instance, modify it using it's API.
* 2. Add `Node` to the model using `Batch` API.
* 3. Change `Node` that was already added to the model using `Batch` API.
*
* Similarly, you cannot use `Batch` API on a node that has not been added to the model tree, with the exception
* of {@link module:engine/model/batch~Batch#insert inserting} that node to the model tree.
*
* Be aware that using {@link module:engine/model/batch~Batch#remove remove from Batch API} does not allow to use `Node` API because
* the information about `Node` is still kept in model document.
*
* In case of {@link module:engine/model/element~Element element node}, adding and removing children also counts as changing a node and
* follows same rules.
*/
class node_Node {
/**
* Creates a model node.
*
* This is an abstract class, so this constructor should not be used directly.
*
* @abstract
* @param {Object} [attrs] Node's attributes. See {@link module:utils/tomap~toMap} for a list of accepted values.
*/
constructor( attrs ) {
/**
* Parent of this node. It could be {@link module:engine/model/element~Element}
* or {@link module:engine/model/documentfragment~DocumentFragment}.
* Equals to `null` if the node has no parent.
*
* @readonly
* @member {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null}
*/
this.parent = null;
/**
* Attributes set on this node.
*
* @private
* @member {Map} module:engine/model/node~Node#_attrs
*/
this._attrs = toMap( attrs );
}
/**
* Index of this node in it's parent or `null` if the node has no parent.
*
* Accessing this property throws an error if this node's parent element does not contain it.
* This means that model tree got broken.
*
* @readonly
* @type {Number|null}
*/
get index() {
let pos;
if ( !this.parent ) {
return null;
}
if ( ( pos = this.parent.getChildIndex( this ) ) === null ) {
throw new CKEditorError( 'model-node-not-found-in-parent: The node\'s parent does not contain this node.' );
}
return pos;
}
/**
* Offset at which this node starts in it's parent. It is equal to the sum of {@link #offsetSize offsetSize}
* of all it's previous siblings. Equals to `null` if node has no parent.
*
* Accessing this property throws an error if this node's parent element does not contain it.
* This means that model tree got broken.
*
* @readonly
* @type {Number|Null}
*/
get startOffset() {
let pos;
if ( !this.parent ) {
return null;
}
if ( ( pos = this.parent.getChildStartOffset( this ) ) === null ) {
throw new CKEditorError( 'model-node-not-found-in-parent: The node\'s parent does not contain this node.' );
}
return pos;
}
/**
* Offset size of this node. Represents how much "offset space" is occupied by the node in it's parent.
* It is important for {@link module:engine/model/position~Position position}. When node has `offsetSize` greater than `1`, position
* can be placed between that node start and end. `offsetSize` greater than `1` is for nodes that represents more
* than one entity, i.e. {@link module:engine/model/text~Text text node}.
*
* @readonly
* @type {Number}
*/
get offsetSize() {
return 1;
}
/**
* Offset at which this node ends in it's parent. It is equal to the sum of this node's
* {@link module:engine/model/node~Node#startOffset start offset} and {@link #offsetSize offset size}.
* Equals to `null` if the node has no parent.
*
* @readonly
* @type {Number|null}
*/
get endOffset() {
if ( !this.parent ) {
return null;
}
return this.startOffset + this.offsetSize;
}
/**
* Node's next sibling or `null` if the node is a last child of it's parent or if the node has no parent.
*
* @readonly
* @type {module:engine/model/node~Node|null}
*/
get nextSibling() {
const index = this.index;
return ( index !== null && this.parent.getChild( index + 1 ) ) || null;
}
/**
* Node's previous sibling or `null` if the node is a first child of it's parent or if the node has no parent.
*
* @readonly
* @type {module:engine/model/node~Node|null}
*/
get previousSibling() {
const index = this.index;
return ( index !== null && this.parent.getChild( index - 1 ) ) || null;
}
/**
* The top-most ancestor of the node. If node has no parent it is the root itself. If the node is a part
* of {@link module:engine/model/documentfragment~DocumentFragment}, it's `root` is equal to that `DocumentFragment`.
*
* @readonly
* @type {module:engine/model/node~Node|module:engine/model/documentfragment~DocumentFragment}
*/
get root() {
let root = this; // eslint-disable-line consistent-this
while ( root.parent ) {
root = root.parent;
}
return root;
}
/**
* {@link module:engine/model/document~Document Document} that owns this node or `null` if the node has no parent or is inside
* a {@link module:engine/model/documentfragment~DocumentFragment DocumentFragment}.
*
* @readonly
* @type {module:engine/model/document~Document|null}
*/
get document() {
// This is a top element of a sub-tree.
if ( this.root == this ) {
return null;
}
// Root may be `DocumentFragment` which does not have document property.
return this.root.document || null;
}
/**
* Creates a copy of this node, that is a node with exactly same attributes, and returns it.
*
* @returns {module:engine/model/node~Node} Node with same attributes as this node.
*/
clone() {
return new node_Node( this._attrs );
}
/**
* Gets path to the node. The path is an array containing starting offsets of consecutive ancestors of this node,
* beginning from {@link module:engine/model/node~Node#root root}, down to this node's starting offset. The path can be used to
* create {@link module:engine/model/position~Position Position} instance.
*
* const abc = new Text( 'abc' );
* const foo = new Text( 'foo' );
* const h1 = new Element( 'h1', null, new Text( 'header' ) );
* const p = new Element( 'p', null, [ abc, foo ] );
* const div = new Element( 'div', null, [ h1, p ] );
* foo.getPath(); // Returns [ 1, 3 ]. `foo` is in `p` which is in `div`. `p` starts at offset 1, while `foo` at 3.
* h1.getPath(); // Returns [ 0 ].
* div.getPath(); // Returns [].
*
* @returns {Array.<Number>} The path.
*/
getPath() {
const path = [];
let node = this; // eslint-disable-line consistent-this
while ( node.parent ) {
path.unshift( node.startOffset );
node = node.parent;
}
return path;
}
/**
* Returns ancestors array of this node.
*
* @param {Object} options Options object.
* @param {Boolean} [options.includeSelf=false] When set to `true` this node will be also included in parent's array.
* @param {Boolean} [options.parentFirst=false] When set to `true`, array will be sorted from node's parent to root element,
* otherwise root element will be the first item in the array.
* @returns {Array} Array with ancestors.
*/
getAncestors( options = { includeSelf: false, parentFirst: false } ) {
const ancestors = [];
let parent = options.includeSelf ? this : this.parent;
while ( parent ) {
ancestors[ options.parentFirst ? 'push' : 'unshift' ]( parent );
parent = parent.parent;
}
return ancestors;
}
/**
* Returns a {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment}
* which is a common ancestor of both nodes.
*
* @param {module:engine/model/node~Node} node The second node.
* @param {Object} options Options object.
* @param {Boolean} [options.includeSelf=false] When set to `true` both nodes will be considered "ancestors" too.
* Which means that if e.g. node A is inside B, then their common ancestor will be B.
* @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null}
*/
getCommonAncestor( node, options = {} ) {
const ancestorsA = this.getAncestors( options );
const ancestorsB = node.getAncestors( options );
let i = 0;
while ( ancestorsA[ i ] == ancestorsB[ i ] && ancestorsA[ i ] ) {
i++;
}
return i === 0 ? null : ancestorsA[ i - 1 ];
}
/**
* Removes this node from it's parent.
*/
remove() {
this.parent.removeChildren( this.index );
}
/**
* Checks if the node has an attribute with given key.
*
* @param {String} key Key of attribute to check.
* @returns {Boolean} `true` if attribute with given key is set on node, `false` otherwise.
*/
hasAttribute( key ) {
return this._attrs.has( key );
}
/**
* Gets an attribute value for given key or `undefined` if that attribute is not set on node.
*
* @param {String} key Key of attribute to look for.
* @returns {*} Attribute value or `undefined`.
*/
getAttribute( key ) {
return this._attrs.get( key );
}
/**
* Returns iterator that iterates over this node's attributes.
*
* Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value.
* This format is accepted by native `Map` object and also can be passed in `Node` constructor.
*
* @returns {Iterable.<*>}
*/
getAttributes() {
return this._attrs.entries();
}
/**
* Returns iterator that iterates over this node's attribute keys.
*
* @returns {Iterator.<String>}
*/
getAttributeKeys() {
return this._attrs.keys();
}
/**
* Sets attribute on the node. If attribute with the same key already is set, it's value is overwritten.
*
* @param {String} key Key of attribute to set.
* @param {*} value Attribute value.
*/
setAttribute( key, value ) {
this._attrs.set( key, value );
}
/**
* Removes all attributes from the node and sets given attributes.
*
* @param {Object} [attrs] Attributes to set. See {@link module:utils/tomap~toMap} for a list of accepted values.
*/
setAttributesTo( attrs ) {
this._attrs = toMap( attrs );
}
/**
* Removes an attribute with given key from the node.
*
* @param {String} key Key of attribute to remove.
* @returns {Boolean} `true` if the attribute was set on the element, `false` otherwise.
*/
removeAttribute( key ) {
return this._attrs.delete( key );
}
/**
* Removes all attributes from the node.
*/
clearAttributes() {
this._attrs.clear();
}
/**
* Converts `Node` to plain object and returns it.
*
* @returns {Object} `Node` converted to plain object.
*/
toJSON() {
const json = {};
if ( this._attrs.size ) {
json.attributes = [ ...this._attrs ];
}
return json;
}
/**
* Checks whether given model tree object is of given type.
*
* This method is useful when processing model tree objects that are of unknown type. For example, a function
* may return {@link module:engine/model/documentfragment~DocumentFragment} or {@link module:engine/model/node~Node}
* that can be either text node or element. This method can be used to check what kind of object is returned.
*
* obj.is( 'node' ); // true for any node, false for document fragment
* obj.is( 'documentFragment' ); // true for document fragment, false for any node
* obj.is( 'element' ); // true for any element, false for text node or document fragment
* obj.is( 'element', 'paragraph' ); // true only for element which name is 'paragraph'
* obj.is( 'paragraph' ); // shortcut for obj.is( 'element', 'paragraph' )
* obj.is( 'text' ); // true for text node, false for element and document fragment
* obj.is( 'textProxy' ); // true for text proxy object
*
* @method #is
* @param {'element'|'rootElement'|'text'|'textProxy'|'documentFragment'} type
* @returns {Boolean}
*/
}
/**
* The node's parent does not contain this node.
*
* This error may be thrown from corrupted trees.
*
* @error model-node-not-found-in-parent
*/
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/text.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/text
*/
/**
* Model text node. Type of {@link module:engine/model/node~Node node} that contains {@link module:engine/model/text~Text#data text data}.
*
* **Important:** see {@link module:engine/model/node~Node} to read about restrictions using `Text` and `Node` API.
*
* **Note:** keep in mind that `Text` instances might indirectly got removed from model tree when model is changed.
* This happens when {@link module:engine/model/writer~writer model writer} is used to change model and the text node is merged with
* another text node. Then, both text nodes are removed and a new text node is inserted into the model. Because of
* this behavior, keeping references to `Text` is not recommended. Instead, consider creating
* {@link module:engine/model/liveposition~LivePosition live position} placed before the text node.
*/
class text_Text extends node_Node {
/**
* Creates a text node.
*
* @param {String} data Node's text.
* @param {Object} [attrs] Node's attributes. See {@link module:utils/tomap~toMap} for a list of accepted values.
*/
constructor( data, attrs ) {
super( attrs );
/**
* Text data contained in this text node.
*
* @type {String}
*/
this.data = data || '';
}
/**
* @inheritDoc
*/
get offsetSize() {
return this.data.length;
}
/**
* @inheritDoc
*/
is( type ) {
return type == 'text';
}
/**
* Creates a copy of this text node and returns it. Created text node has same text data and attributes as original text node.
*/
clone() {
return new text_Text( this.data, this.getAttributes() );
}
/**
* Converts `Text` instance to plain object and returns it.
*
* @returns {Object} `Text` instance converted to plain object.
*/
toJSON() {
const json = super.toJSON();
json.data = this.data;
return json;
}
/**
* Creates a `Text` instance from given plain object (i.e. parsed JSON string).
*
* @param {Object} json Plain object to be converted to `Text`.
* @returns {module:engine/model/text~Text} `Text` instance created using given plain object.
*/
static fromJSON( json ) {
return new text_Text( json.data, json.attributes );
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/textproxy.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/textproxy
*/
/**
* `TextProxy` represents a part of {@link module:engine/model/text~Text text node}.
*
* Since {@link module:engine/model/position~Position positions} can be placed between characters of a text node,
* {@link module:engine/model/range~Range ranges} may contain only parts of text nodes. When {@link module:engine/model/range~Range#getItems
* getting items}
* contained in such range, we need to represent a part of that text node, since returning the whole text node would be incorrect.
* `TextProxy` solves this issue.
*
* `TextProxy` has an API similar to {@link module:engine/model/text~Text Text} and allows to do most of the common tasks performed
* on model nodes.
*
* **Note:** Some `TextProxy` instances may represent whole text node, not just a part of it.
* See {@link module:engine/model/textproxy~TextProxy#isPartial}.
*
* **Note:** `TextProxy` is not an instance of {@link module:engine/model/node~Node node}. Keep this in mind when using it as a
* parameter of methods.
*
* **Note:** `TextProxy` is a readonly interface. If you want to perform changes on model data represented by a `TextProxy`
* use {@link module:engine/model/writer~writer model writer API}.
*
* **Note:** `TextProxy` instances are created on the fly, basing on the current state of model. Because of this, it is
* highly unrecommended to store references to `TextProxy` instances. `TextProxy` instances are not refreshed when
* model changes, so they might get invalidated. Instead, consider creating {@link module:engine/model/liveposition~LivePosition live
* position}.
*
* `TextProxy` instances are created by {@link module:engine/model/treewalker~TreeWalker model tree walker}. You should not need to create
* an instance of this class by your own.
*/
class textproxy_TextProxy {
/**
* Creates a text proxy.
*
* @protected
* @param {module:engine/model/text~Text} textNode Text node which part is represented by this text proxy.
* @param {Number} offsetInText Offset in {@link module:engine/model/textproxy~TextProxy#textNode text node} from which the text proxy
* starts.
* @param {Number} length Text proxy length, that is how many text node's characters, starting from `offsetInText` it represents.
* @constructor
*/
constructor( textNode, offsetInText, length ) {
/**
* Text node which part is represented by this text proxy.
*
* @readonly
* @member {module:engine/model/text~Text}
*/
this.textNode = textNode;
if ( offsetInText < 0 || offsetInText > textNode.offsetSize ) {
/**
* Given `offsetInText` value is incorrect.
*
* @error model-textproxy-wrong-offsetintext
*/
throw new CKEditorError( 'model-textproxy-wrong-offsetintext: Given offsetInText value is incorrect.' );
}
if ( length < 0 || offsetInText + length > textNode.offsetSize ) {
/**
* Given `length` value is incorrect.
*
* @error model-textproxy-wrong-length
*/
throw new CKEditorError( 'model-textproxy-wrong-length: Given length value is incorrect.' );
}
/**
* Text data represented by this text proxy.
*
* @readonly
* @member {String}
*/
this.data = textNode.data.substring( offsetInText, offsetInText + length );
/**
* Offset in {@link module:engine/model/textproxy~TextProxy#textNode text node} from which the text proxy starts.
*
* @readonly
* @member {Number}
*/
this.offsetInText = offsetInText;
}
/**
* Offset at which this text proxy starts in it's parent.
*
* @see module:engine/model/node~Node#startOffset
* @readonly
* @type {Number}
*/
get startOffset() {
return this.textNode.startOffset !== null ? this.textNode.startOffset + this.offsetInText : null;
}
/**
* Offset size of this text proxy. Equal to the number of characters represented by the text proxy.
*
* @see module:engine/model/node~Node#offsetSize
* @readonly
* @type {Number}
*/
get offsetSize() {
return this.data.length;
}
/**
* Offset at which this text proxy ends in it's parent.
*
* @see module:engine/model/node~Node#endOffset
* @readonly
* @type {Number}
*/
get endOffset() {
return this.startOffset !== null ? this.startOffset + this.offsetSize : null;
}
/**
* Flag indicating whether `TextProxy` instance covers only part of the original {@link module:engine/model/text~Text text node}
* (`true`) or the whole text node (`false`).
*
* This is `false` when text proxy starts at the very beginning of {@link module:engine/model/textproxy~TextProxy#textNode textNode}
* ({@link module:engine/model/textproxy~TextProxy#offsetInText offsetInText} equals `0`) and text proxy sizes is equal to
* text node size.
*
* @readonly
* @type {Boolean}
*/
get isPartial() {
return this.offsetSize !== this.textNode.offsetSize;
}
/**
* Parent of this text proxy, which is same as parent of text node represented by this text proxy.
*
* @readonly
* @type {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null}
*/
get parent() {
return this.textNode.parent;
}
/**
* Root of this text proxy, which is same as root of text node represented by this text proxy.
*
* @readonly
* @type {module:engine/model/node~Node|module:engine/model/documentfragment~DocumentFragment}
*/
get root() {
return this.textNode.root;
}
/**
* {@link module:engine/model/document~Document Document} that owns text node represented by this text proxy or `null` if the text node
* has no parent or is inside a {@link module:engine/model/documentfragment~DocumentFragment DocumentFragment}.
*
* @readonly
* @type {module:engine/model/document~Document|null}
*/
get document() {
return this.textNode.document;
}
/**
* Checks whether given model tree object is of given type.
*
* Read more in {@link module:engine/model/node~Node#is}.
*
* @param {String} type
* @returns {Boolean}
*/
is( type ) {
return type == 'textProxy';
}
/**
* Gets path to this text proxy.
*
* @see module:engine/model/node~Node#getPath
* @returns {Array.<Number>}
*/
getPath() {
const path = this.textNode.getPath();
if ( path.length > 0 ) {
path[ path.length - 1 ] += this.offsetInText;
}
return path;
}
/**
* Returns ancestors array of this text proxy.
*
* @param {Object} options Options object.
* @param {Boolean} [options.includeSelf=false] When set to `true` this text proxy will be also included in parent's array.
* @param {Boolean} [options.parentFirst=false] When set to `true`, array will be sorted from text proxy parent to root element,
* otherwise root element will be the first item in the array.
* @returns {Array} Array with ancestors.
*/
getAncestors( options = { includeSelf: false, parentFirst: false } ) {
const ancestors = [];
let parent = options.includeSelf ? this : this.parent;
while ( parent ) {
ancestors[ options.parentFirst ? 'push' : 'unshift' ]( parent );
parent = parent.parent;
}
return ancestors;
}
/**
* Checks if this text proxy has an attribute for given key.
*
* @param {String} key Key of attribute to check.
* @returns {Boolean} `true` if attribute with given key is set on text proxy, `false` otherwise.
*/
hasAttribute( key ) {
return this.textNode.hasAttribute( key );
}
/**
* Gets an attribute value for given key or `undefined` if that attribute is not set on text proxy.
*
* @param {String} key Key of attribute to look for.
* @returns {*} Attribute value or `undefined`.
*/
getAttribute( key ) {
return this.textNode.getAttribute( key );
}
/**
* Returns iterator that iterates over this node's attributes. Attributes are returned as arrays containing two
* items. First one is attribute key and second is attribute value.
*
* This format is accepted by native `Map` object and also can be passed in `Node` constructor.
*
* @returns {Iterable.<*>}
*/
getAttributes() {
return this.textNode.getAttributes();
}
/**
* Returns iterator that iterates over this node's attribute keys.
*
* @returns {Iterator.<String>}
*/
getAttributeKeys() {
return this.textNode.getAttributeKeys();
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/nodelist.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/nodelist
*/
/**
* Provides an interface to operate on a list of {@link module:engine/model/node~Node nodes}. `NodeList` is used internally
* in classes like {@link module:engine/model/element~Element Element}
* or {@link module:engine/model/documentfragment~DocumentFragment DocumentFragment}.
*/
class nodelist_NodeList {
/**
* Creates an empty node list.
*
* @param {Iterable.<module:engine/model/node~Node>} nodes Nodes contained in this node list.
*/
constructor( nodes ) {
/**
* Nodes contained in this node list.
*
* @private
* @member {Array.<module:engine/model/node~Node>}
*/
this._nodes = [];
if ( nodes ) {
this.insertNodes( 0, nodes );
}
}
/**
* Returns an iterator that iterates over all nodes contained inside this node list.
*
* @returns {Iterator.<module:engine/model/node~Node>}
*/
[ Symbol.iterator ]() {
return this._nodes[ Symbol.iterator ]();
}
/**
* Number of nodes contained inside this node list.
*
* @readonly
* @type {Number}
*/
get length() {
return this._nodes.length;
}
/**
* Sum of {@link module:engine/model/node~Node#offsetSize offset sizes} of all nodes contained inside this node list.
*
* @readonly
* @type {Number}
*/
get maxOffset() {
return this._nodes.reduce( ( sum, node ) => sum + node.offsetSize, 0 );
}
/**
* Gets the node at the given index. Returns `null` if incorrect index was passed.
*
* @param {Number} index Index of node.
* @returns {module:engine/model/node~Node|null} Node at given index.
*/
getNode( index ) {
return this._nodes[ index ] || null;
}
/**
* Returns an index of the given node. Returns `null` if given node is not inside this node list.
*
* @param {module:engine/model/node~Node} node Child node to look for.
* @returns {Number|null} Child node's index.
*/
getNodeIndex( node ) {
const index = this._nodes.indexOf( node );
return index == -1 ? null : index;
}
/**
* Returns the starting offset of given node. Starting offset is equal to the sum of
* {module:engine/model/node~Node#offsetSize offset sizes} of all nodes that are before this node in this node list.
*
* @param {module:engine/model/node~Node} node Node to look for.
* @returns {Number|null} Node's starting offset.
*/
getNodeStartOffset( node ) {
const index = this.getNodeIndex( node );
return index === null ? null : this._nodes.slice( 0, index ).reduce( ( sum, node ) => sum + node.offsetSize, 0 );
}
/**
* Converts index to offset in node list.
*
* Returns starting offset of a node that is at given index. Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
* `model-nodelist-index-out-of-bounds` if given index is less than `0` or more than {@link #length}.
*
* @param {Number} index Node's index.
* @returns {Number} Node's starting offset.
*/
indexToOffset( index ) {
if ( index == this._nodes.length ) {
return this.maxOffset;
}
const node = this._nodes[ index ];
if ( !node ) {
/**
* Given index cannot be found in the node list.
*
* @error nodelist-index-out-of-bounds
*/
throw new CKEditorError( 'model-nodelist-index-out-of-bounds: Given index cannot be found in the node list.' );
}
return this.getNodeStartOffset( node );
}
/**
* Converts offset in node list to index.
*
* Returns index of a node that occupies given offset. Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
* `model-nodelist-offset-out-of-bounds` if given offset is less than `0` or more than {@link #maxOffset}.
*
* @param {Number} offset Offset to look for.
* @returns {Number} Index of a node that occupies given offset.
*/
offsetToIndex( offset ) {
let totalOffset = 0;
for ( const node of this._nodes ) {
if ( offset >= totalOffset && offset < totalOffset + node.offsetSize ) {
return this.getNodeIndex( node );
}
totalOffset += node.offsetSize;
}
if ( totalOffset != offset ) {
/**
* Given offset cannot be found in the node list.
*
* @error nodelist-offset-out-of-bounds
*/
throw new CKEditorError( 'model-nodelist-offset-out-of-bounds: Given offset cannot be found in the node list.' );
}
return this.length;
}
/**
* Inserts given nodes at given index.
*
* @param {Number} index Index at which nodes should be inserted.
* @param {Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted.
*/
insertNodes( index, nodes ) {
// Validation.
for ( const node of nodes ) {
if ( !( node instanceof node_Node ) ) {
/**
* Trying to insert an object which is not a Node instance.
*
* @error nodelist-insertNodes-not-node
*/
throw new CKEditorError( 'model-nodelist-insertNodes-not-node: Trying to insert an object which is not a Node instance.' );
}
}
this._nodes.splice( index, 0, ...nodes );
}
/**
* Removes one or more nodes starting at the given index.
*
* @param {Number} indexStart Index of the first node to remove.
* @param {Number} [howMany=1] Number of nodes to remove.
* @returns {Array.<module:engine/model/node~Node>} Array containing removed nodes.
*/
removeNodes( indexStart, howMany = 1 ) {
return this._nodes.splice( indexStart, howMany );
}
/**
* Converts `NodeList` instance to an array containing nodes that were inserted in the node list. Nodes
* are also converted to their plain object representation.
*
* @returns {Array.<module:engine/model/node~Node>} `NodeList` instance converted to `Array`.
*/
toJSON() {
return this._nodes.map( node => node.toJSON() );
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/isiterable.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module utils/isiterable
*/
/**
* Checks if value implements iterator interface.
*
* @param {*} value The value to check.
* @returns {Boolean} True if value implements iterator interface.
*/
function isIterable( value ) {
return !!( value && value[ Symbol.iterator ] );
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/element.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/element
*/
/**
* Model element. Type of {@link module:engine/model/node~Node node} that has a {@link module:engine/model/element~Element#name name} and
* {@link module:engine/model/element~Element#getChildren child nodes}.
*
* **Important**: see {@link module:engine/model/node~Node} to read about restrictions using `Element` and `Node` API.
*/
class element_Element extends node_Node {
/**
* Creates a model element.
*
* @param {String} name Element's name.
* @param {Object} [attrs] Element's attributes. See {@link module:utils/tomap~toMap} for a list of accepted values.
* @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} [children]
* One or more nodes to be inserted as children of created element.
*/
constructor( name, attrs, children ) {
super( attrs );
/**
* Element name.
*
* @member {String} module:engine/model/element~Element#name
*/
this.name = name;
/**
* List of children nodes.
*
* @private
* @member {module:engine/model/nodelist~NodeList} module:engine/model/element~Element#_children
*/
this._children = new nodelist_NodeList();
if ( children ) {
this.insertChildren( 0, children );
}
}
/**
* Number of this element's children.
*
* @readonly
* @type {Number}
*/
get childCount() {
return this._children.length;
}
/**
* Sum of {module:engine/model/node~Node#offsetSize offset sizes} of all of this element's children.
*
* @readonly
* @type {Number}
*/
get maxOffset() {
return this._children.maxOffset;
}
/**
* Is `true` if there are no nodes inside this element, `false` otherwise.
*
* @readonly
* @type {Boolean}
*/
get isEmpty() {
return this.childCount === 0;
}
/**
* Checks whether given model tree object is of given type.
*
* obj.name; // 'listItem'
* obj instanceof Element; // true
*
* obj.is( 'element' ); // true
* obj.is( 'listItem' ); // true
* obj.is( 'element', 'listItem' ); // true
* obj.is( 'text' ); // false
* obj.is( 'element', 'image' ); // false
*
* Read more in {@link module:engine/model/node~Node#is}.
*
* @param {String} type Type to check when `name` parameter is present.
* Otherwise, it acts like the `name` parameter.
* @param {String} [name] Element name.
* @returns {Boolean}
*/
is( type, name = null ) {
if ( !name ) {
return type == 'element' || type == this.name;
} else {
return type == 'element' && name == this.name;
}
}
/**
* Gets the child at the given index.
*
* @param {Number} index Index of child.
* @returns {module:engine/model/node~Node} Child node.
*/
getChild( index ) {
return this._children.getNode( index );
}
/**
* Returns an iterator that iterates over all of this element's children.
*
* @returns {Iterable.<module:engine/model/node~Node>}
*/
getChildren() {
return this._children[ Symbol.iterator ]();
}
/**
* Returns an index of the given child node. Returns `null` if given node is not a child of this element.
*
* @param {module:engine/model/node~Node} node Child node to look for.
* @returns {Number} Child node's index in this element.
*/
getChildIndex( node ) {
return this._children.getNodeIndex( node );
}
/**
* Returns the starting offset of given child. Starting offset is equal to the sum of
* {module:engine/model/node~Node#offsetSize offset sizes} of all node's siblings that are before it. Returns `null` if
* given node is not a child of this element.
*
* @param {module:engine/model/node~Node} node Child node to look for.
* @returns {Number} Child node's starting offset.
*/
getChildStartOffset( node ) {
return this._children.getNodeStartOffset( node );
}
/**
* Creates a copy of this element and returns it. Created element has the same name and attributes as the original element.
* If clone is deep, the original element's children are also cloned. If not, then empty element is removed.
*
* @param {Boolean} [deep=false] If set to `true` clones element and all its children recursively. When set to `false`,
* element will be cloned without any child.
*/
clone( deep = false ) {
const children = deep ? Array.from( this._children ).map( node => node.clone( true ) ) : null;
return new element_Element( this.name, this.getAttributes(), children );
}
/**
* Returns index of a node that occupies given offset. If given offset is too low, returns `0`. If given offset is
* too high, returns {@link module:engine/model/element~Element#getChildIndex index after last child}.
*
* const textNode = new Text( 'foo' );
* const pElement = new Element( 'p' );
* const divElement = new Element( [ textNode, pElement ] );
* divElement.offsetToIndex( -1 ); // Returns 0, because offset is too low.
* divElement.offsetToIndex( 0 ); // Returns 0, because offset 0 is taken by `textNode` which is at index 0.
* divElement.offsetToIndex( 1 ); // Returns 0, because `textNode` has `offsetSize` equal to 3, so it occupies offset 1 too.
* divElement.offsetToIndex( 2 ); // Returns 0.
* divElement.offsetToIndex( 3 ); // Returns 1.
* divElement.offsetToIndex( 4 ); // Returns 2. There are no nodes at offset 4, so last available index is returned.
*
* @param {Number} offset Offset to look for.
* @returns {Number}
*/
offsetToIndex( offset ) {
return this._children.offsetToIndex( offset );
}
/**
* {@link module:engine/model/element~Element#insertChildren Inserts} one or more nodes at the end of this element.
*
* @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted.
*/
appendChildren( nodes ) {
this.insertChildren( this.childCount, nodes );
}
/**
* Inserts one or more nodes at the given index and sets {@link module:engine/model/node~Node#parent parent} of these nodes
* to this element.
*
* @param {Number} index Index at which nodes should be inserted.
* @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted.
*/
insertChildren( index, nodes ) {
nodes = normalize( nodes );
for ( const node of nodes ) {
// If node that is being added to this element is already inside another element, first remove it from the old parent.
if ( node.parent !== null ) {
node.remove();
}
node.parent = this;
}
this._children.insertNodes( index, nodes );
}
/**
* Removes one or more nodes starting at the given index and sets
* {@link module:engine/model/node~Node#parent parent} of these nodes to `null`.
*
* @param {Number} index Index of the first node to remove.
* @param {Number} [howMany=1] Number of nodes to remove.
* @returns {Array.<module:engine/model/node~Node>} Array containing removed nodes.
*/
removeChildren( index, howMany = 1 ) {
const nodes = this._children.removeNodes( index, howMany );
for ( const node of nodes ) {
node.parent = null;
}
return nodes;
}
/**
* Returns a descendant node by its path relative to this element.
*
* // <this>a<b>c</b></this>
* this.getNodeByPath( [ 0 ] ); // -> "a"
* this.getNodeByPath( [ 1 ] ); // -> <b>
* this.getNodeByPath( [ 1, 0 ] ); // -> "c"
*
* @param {Array.<Number>} relativePath Path of the node to find, relative to this element.
* @returns {module:engine/model/node~Node}
*/
getNodeByPath( relativePath ) {
let node = this; // eslint-disable-line consistent-this
for ( const index of relativePath ) {
node = node.getChild( node.offsetToIndex( index ) );
}
return node;
}
/**
* Converts `Element` instance to plain object and returns it. Takes care of converting all of this element's children.
*
* @returns {Object} `Element` instance converted to plain object.
*/
toJSON() {
const json = super.toJSON();
json.name = this.name;
if ( this._children.length > 0 ) {
json.children = [];
for ( const node of this._children ) {
json.children.push( node.toJSON() );
}
}
return json;
}
/**
* Creates an `Element` instance from given plain object (i.e. parsed JSON string).
* Converts `Element` children to proper nodes.
*
* @param {Object} json Plain object to be converted to `Element`.
* @returns {module:engine/model/element~Element} `Element` instance created using given plain object.
*/
static fromJSON( json ) {
let children = null;
if ( json.children ) {
children = [];
for ( const child of json.children ) {
if ( child.name ) {
// If child has name property, it is an Element.
children.push( element_Element.fromJSON( child ) );
} else {
// Otherwise, it is a Text node.
children.push( text_Text.fromJSON( child ) );
}
}
}
return new element_Element( json.name, json.attributes, children );
}
}
// Converts strings to Text and non-iterables to arrays.
//
// @param {String|module:engine/model/node~Node|Iterable.<String|module:engine/model/node~Node>}
// @return {Iterable.<module:engine/model/node~Node>}
function normalize( nodes ) {
// Separate condition because string is iterable.
if ( typeof nodes == 'string' ) {
return [ new text_Text( nodes ) ];
}
if ( !isIterable( nodes ) ) {
nodes = [ nodes ];
}
// Array.from to enable .map() on non-arrays.
return Array.from( nodes )
.map( node => {
return typeof node == 'string' ? new text_Text( node ) : node;
} );
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/treewalker.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/treewalker
*/
/**
* Position iterator class. It allows to iterate forward and backward over the document.
*/
class treewalker_TreeWalker {
/**
* Creates a range iterator. All parameters are optional, but you have to specify either `boundaries` or `startPosition`.
*
* @constructor
* @param {Object} [options={}] Object with configuration.
* @param {'forward'|'backward'} [options.direction='forward'] Walking direction.
* @param {module:engine/model/range~Range} [options.boundaries=null] Range to define boundaries of the iterator.
* @param {module:engine/model/position~Position} [options.startPosition] Starting position.
* @param {Boolean} [options.singleCharacters=false] Flag indicating whether all consecutive characters with the same attributes
* should be returned one by one as multiple {@link module:engine/model/textproxy~TextProxy} (`true`) objects or as one
* {@link module:engine/model/textproxy~TextProxy} (`false`).
* @param {Boolean} [options.shallow=false] Flag indicating whether iterator should enter elements or not. If the
* iterator is shallow child nodes of any iterated node will not be returned along with `elementEnd` tag.
* @param {Boolean} [options.ignoreElementEnd=false] Flag indicating whether iterator should ignore `elementEnd`
* tags. If the option is true walker will not return a parent node of start position. If this option is `true`
* each {@link module:engine/model/element~Element} will be returned once, while if the option is `false` they might be returned
* twice: for `'elementStart'` and `'elementEnd'`.
*/
constructor( options = {} ) {
if ( !options.boundaries && !options.startPosition ) {
/**
* Neither boundaries nor starting position of a `TreeWalker` have been defined.
*
* @error model-tree-walker-no-start-position
*/
throw new CKEditorError( 'model-tree-walker-no-start-position: Neither boundaries nor starting position have been defined.' );
}
const direction = options.direction || 'forward';
if ( direction != 'forward' && direction != 'backward' ) {
throw new CKEditorError(
'model-tree-walker-unknown-direction: Only `backward` and `forward` direction allowed.',
{ direction }
);
}
/**
* Walking direction. Defaults `'forward'`.
*
* @readonly
* @member {'backward'|'forward'} module:engine/model/treewalker~TreeWalker#direction
*/
this.direction = direction;
/**
* Iterator boundaries.
*
* When the iterator is walking `'forward'` on the end of boundary or is walking `'backward'`
* on the start of boundary, then `{ done: true }` is returned.
*
* If boundaries are not defined they are set before first and after last child of the root node.
*
* @readonly
* @member {module:engine/model/range~Range} module:engine/model/treewalker~TreeWalker#boundaries
*/
this.boundaries = options.boundaries || null;
/**
* Iterator position. This is always static position, even if the initial position was a
* {@link module:engine/model/liveposition~LivePosition live position}. If start position is not defined then position depends
* on {@link #direction}. If direction is `'forward'` position starts form the beginning, when direction
* is `'backward'` position starts from the end.
*
* @readonly
* @member {module:engine/model/position~Position} module:engine/model/treewalker~TreeWalker#position
*/
if ( options.startPosition ) {
this.position = position_Position.createFromPosition( options.startPosition );
} else {
this.position = position_Position.createFromPosition( this.boundaries[ this.direction == 'backward' ? 'end' : 'start' ] );
}
/**
* Flag indicating whether all consecutive characters with the same attributes should be
* returned as one {@link module:engine/model/textproxy~TextProxy} (`true`) or one by one (`false`).
*
* @readonly
* @member {Boolean} module:engine/model/treewalker~TreeWalker#singleCharacters
*/
this.singleCharacters = !!options.singleCharacters;
/**
* Flag indicating whether iterator should enter elements or not. If the iterator is shallow child nodes of any
* iterated node will not be returned along with `elementEnd` tag.
*
* @readonly
* @member {Boolean} module:engine/model/treewalker~TreeWalker#shallow
*/
this.shallow = !!options.shallow;
/**
* Flag indicating whether iterator should ignore `elementEnd` tags. If the option is true walker will not
* return a parent node of the start position. If this option is `true` each {@link module:engine/model/element~Element} will
* be returned once, while if the option is `false` they might be returned twice:
* for `'elementStart'` and `'elementEnd'`.
*
* @readonly
* @member {Boolean} module:engine/model/treewalker~TreeWalker#ignoreElementEnd
*/
this.ignoreElementEnd = !!options.ignoreElementEnd;
/**
* Start boundary cached for optimization purposes.
*
* @private
* @member {module:engine/model/element~Element} module:engine/model/treewalker~TreeWalker#_boundaryStartParent
*/
this._boundaryStartParent = this.boundaries ? this.boundaries.start.parent : null;
/**
* End boundary cached for optimization purposes.
*
* @private
* @member {module:engine/model/element~Element} module:engine/model/treewalker~TreeWalker#_boundaryEndParent
*/
this._boundaryEndParent = this.boundaries ? this.boundaries.end.parent : null;
/**
* Parent of the most recently visited node. Cached for optimization purposes.
*
* @private
* @member {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
* module:engine/model/treewalker~TreeWalker#_visitedParent
*/
this._visitedParent = this.position.parent;
}
/**
* Iterator interface.
*/
[ Symbol.iterator ]() {
return this;
}
/**
* Moves {@link #position} in the {@link #direction} skipping values as long as the callback function returns `true`.
*
* For example:
*
* walker.skip( value => value.type == 'text' ); // <paragraph>[]foo</paragraph> -> <paragraph>foo[]</paragraph>
* walker.skip( () => true ); // Move the position to the end: <paragraph>[]foo</paragraph> -> <paragraph>foo</paragraph>[]
* walker.skip( () => false ); // Do not move the position.
*
* @param {Function} skip Callback function. Gets {@link module:engine/model/treewalker~TreeWalkerValue} and should
* return `true` if the value should be skipped or `false` if not.
*/
skip( skip ) {
let done, value, prevPosition, prevVisitedParent;
do {
prevPosition = this.position;
prevVisitedParent = this._visitedParent;
( { done, value } = this.next() );
} while ( !done && skip( value ) );
if ( !done ) {
this.position = prevPosition;
this._visitedParent = prevVisitedParent;
}
}
/**
* Iterator interface method.
* Detects walking direction and makes step forward or backward.
*
* @returns {Object} Object implementing iterator interface, returning information about taken step.
*/
next() {
if ( this.direction == 'forward' ) {
return this._next();
} else {
return this._previous();
}
}
/**
* Makes a step forward in model. Moves the {@link #position} to the next position and returns the encountered value.
*
* @private
* @returns {Object}
* @returns {Boolean} return.done True if iterator is done.
* @returns {module:engine/model/treewalker~TreeWalkerValue} return.value Information about taken step.
*/
_next() {
const previousPosition = this.position;
const position = position_Position.createFromPosition( this.position );
const parent = this._visitedParent;
// We are at the end of the root.
if ( parent.parent === null && position.offset === parent.maxOffset ) {
return { done: true };
}
// We reached the walker boundary.
if ( parent === this._boundaryEndParent && position.offset == this.boundaries.end.offset ) {
return { done: true };
}
const node = position.textNode ? position.textNode : position.nodeAfter;
if ( node instanceof element_Element ) {
if ( !this.shallow ) {
// Manual operations on path internals for optimization purposes. Here and in the rest of the method.
position.path.push( 0 );
this._visitedParent = node;
} else {
position.offset++;
}
this.position = position;
return formatReturnValue( 'elementStart', node, previousPosition, position, 1 );
} else if ( node instanceof text_Text ) {
let charactersCount;
if ( this.singleCharacters ) {
charactersCount = 1;
} else {
let offset = node.endOffset;
if ( this._boundaryEndParent == parent && this.boundaries.end.offset < offset ) {
offset = this.boundaries.end.offset;
}
charactersCount = offset - position.offset;
}
const offsetInTextNode = position.offset - node.startOffset;
const item = new textproxy_TextProxy( node, offsetInTextNode, charactersCount );
position.offset += charactersCount;
this.position = position;
return formatReturnValue( 'text', item, previousPosition, position, charactersCount );
} else {
// `node` is not set, we reached the end of current `parent`.
position.path.pop();
position.offset++;
this.position = position;
this._visitedParent = parent.parent;
if ( this.ignoreElementEnd ) {
return this._next();
} else {
return formatReturnValue( 'elementEnd', parent, previousPosition, position );
}
}
}
/**
* Makes a step backward in model. Moves the {@link #position} to the previous position and returns the encountered value.
*
* @private
* @returns {Object}
* @returns {Boolean} return.done True if iterator is done.
* @returns {module:engine/model/treewalker~TreeWalkerValue} return.value Information about taken step.
*/
_previous() {
const previousPosition = this.position;
const position = position_Position.createFromPosition( this.position );
const parent = this._visitedParent;
// We are at the beginning of the root.
if ( parent.parent === null && position.offset === 0 ) {
return { done: true };
}
// We reached the walker boundary.
if ( parent == this._boundaryStartParent && position.offset == this.boundaries.start.offset ) {
return { done: true };
}
// Get node just before current position
const node = position.textNode ? position.textNode : position.nodeBefore;
if ( node instanceof element_Element ) {
position.offset--;
if ( !this.shallow ) {
position.path.push( node.maxOffset );
this.position = position;
this._visitedParent = node;
if ( this.ignoreElementEnd ) {
return this._previous();
} else {
return formatReturnValue( 'elementEnd', node, previousPosition, position );
}
} else {
this.position = position;
return formatReturnValue( 'elementStart', node, previousPosition, position, 1 );
}
} else if ( node instanceof text_Text ) {
let charactersCount;
if ( this.singleCharacters ) {
charactersCount = 1;
} else {
let offset = node.startOffset;
if ( this._boundaryStartParent == parent && this.boundaries.start.offset > offset ) {
offset = this.boundaries.start.offset;
}
charactersCount = position.offset - offset;
}
const offsetInTextNode = position.offset - node.startOffset;
const item = new textproxy_TextProxy( node, offsetInTextNode - charactersCount, charactersCount );
position.offset -= charactersCount;
this.position = position;
return formatReturnValue( 'text', item, previousPosition, position, charactersCount );
} else {
// `node` is not set, we reached the beginning of current `parent`.
position.path.pop();
this.position = position;
this._visitedParent = parent.parent;
return formatReturnValue( 'elementStart', parent, previousPosition, position, 1 );
}
}
}
function formatReturnValue( type, item, previousPosition, nextPosition, length ) {
return {
done: false,
value: {
type,
item,
previousPosition,
nextPosition,
length
}
};
}
/**
* Type of the step made by {@link module:engine/model/treewalker~TreeWalker}.
* Possible values: `'elementStart'` if walker is at the beginning of a node, `'elementEnd'` if walker is at the end of node,
* `'character'` if walker traversed over a character, or `'text'` if walker traversed over multiple characters (available in
* character merging mode, see {@link module:engine/model/treewalker~TreeWalker#constructor}).
*
* @typedef {'elementStart'|'elementEnd'|'character'|'text'} module:engine/model/treewalker~TreeWalkerValueType
*/
/**
* Object returned by {@link module:engine/model/treewalker~TreeWalker} when traversing tree model.
*
* @typedef {Object} module:engine/model/treewalker~TreeWalkerValue
* @property {module:engine/model/treewalker~TreeWalkerValueType} type
* @property {module:engine/model/item~Item} item Item between old and new positions of {@link module:engine/model/treewalker~TreeWalker}.
* @property {module:engine/model/position~Position} previousPosition Previous position of the iterator.
* * Forward iteration: For `'elementEnd'` it is the last position inside the element. For all other types it is the
* position before the item. Note that it is more efficient to use this position then calculate the position before
* the node using {@link module:engine/model/position~Position.createBefore}. It is also more efficient to get the
* position after node by shifting `previousPosition` by `length`, using {@link module:engine/model/position~Position#getShiftedBy},
* then calculate the position using {@link module:engine/model/position~Position.createAfter}.
* * Backward iteration: For `'elementStart'` it is the first position inside the element. For all other types it is
* the position after item.
* @property {module:engine/model/position~Position} nextPosition Next position of the iterator.
* * Forward iteration: For `'elementStart'` it is the first position inside the element. For all other types it is
* the position after the item.
* * Backward iteration: For `'elementEnd'` it is last position inside element. For all other types it is the position
* before the item.
* @property {Number} [length] Length of the item. For `'elementStart'` and `'character'` it is 1. For `'text'` it is
* the length of the text. For `'elementEnd'` it is undefined.
*/
/**
* Tree walking directions.
*
* @typedef {'forward'|'backward'} module:engine/view/treewalker~TreeWalkerDirection
*/
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/last.js
/**
* Gets the last element of `array`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to query.
* @returns {*} Returns the last element of `array`.
* @example
*
* _.last([1, 2, 3]);
* // => 3
*/
function last_last(array) {
var length = array ? array.length : 0;
return length ? array[length - 1] : undefined;
}
/* harmony default export */ var lodash_last = (last_last);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/comparearrays.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module utils/comparearrays
*/
/**
* Compares how given arrays relate to each other. One array can be: same as another array, prefix of another array
* or completely different. If arrays are different, first index at which they differ is returned. Otherwise,
* a flag specifying the relation is returned. Flags are negative numbers, so whenever a number >= 0 is returned
* it means that arrays differ.
*
* compareArrays( [ 0, 2 ], [ 0, 2 ] ); // 'same'
* compareArrays( [ 0, 2 ], [ 0, 2, 1 ] ); // 'prefix'
* compareArrays( [ 0, 2 ], [ 0 ] ); // 'extension'
* compareArrays( [ 0, 2 ], [ 1, 2 ] ); // 0
* compareArrays( [ 0, 2 ], [ 0, 1 ] ); // 1
*
* @param {Array} a Array that is compared.
* @param {Array} b Array to compare with.
* @returns {module:utils/comparearrays~ArrayRelation} How array `a` is related to `b`.
*/
function compareArrays( a, b ) {
const minLen = Math.min( a.length, b.length );
for ( let i = 0; i < minLen; i++ ) {
if ( a[ i ] != b[ i ] ) {
// The arrays are different.
return i;
}
}
// Both arrays were same at all points.
if ( a.length == b.length ) {
// If their length is also same, they are the same.
return 'same';
} else if ( a.length < b.length ) {
// Compared array is shorter so it is a prefix of the other array.
return 'prefix';
} else {
// Compared array is longer so it is an extension of the other array.
return 'extension';
}
}
/**
* @typedef {'extension'|'same'|'prefix'} module:utils/comparearrays~ArrayRelation
*/
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/position.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/position
*/
/**
* Represents a position in the model tree.
*
* **Note:** Position is based on offsets, not indexes. This means that position in element containing two text nodes
* with data `foo` and `bar`, position between them has offset `3`, not `1`.
* See {@link module:engine/model/position~Position#path} for more.
*
* Since position in a model is represented by a {@link module:engine/model/position~Position#root position root} and
* {@link module:engine/model/position~Position#path position path} it is possible to create positions placed in non-existing elements.
* This requirement is important for {@link module:engine/model/operation/transform~transform operational transformation}.
*
* Also, {@link module:engine/model/operation/operation~Operation operations}
* kept in {@link module:engine/model/document~Document#history document history}
* are storing positions (and ranges) which were correct when those operations were applied, but may not be correct
* after document got changed.
*
* When changes are applied to model, it may also happen that {@link module:engine/model/position~Position#parent position parent}
* will change even if position path has not changed. Keep in mind, that if a position leads to non-existing element,
* {@link module:engine/model/position~Position#parent} and some other properties and methods will throw errors.
*
* In most cases, position with wrong path is caused by an error in code, but it is sometimes needed, as described above.
*/
class position_Position {
/**
* Creates a position.
*
* @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} root Root of the position.
* @param {Array.<Number>} path Position path. See {@link module:engine/model/position~Position#path}.
*/
constructor( root, path ) {
if ( !root.is( 'element' ) && !root.is( 'documentFragment' ) ) {
/**
* Position root is invalid.
*
* Positions can only be anchored in elements or document fragments.
*
* @error model-position-root-invalid
*/
throw new CKEditorError( 'model-position-root-invalid: Position root invalid.' );
}
if ( !( path instanceof Array ) || path.length === 0 ) {
/**
* Position path must be an array with at least one item.
*
* @error model-position-path-incorrect
* @param path
*/
throw new CKEditorError( 'model-position-path-incorrect: Position path must be an array with at least one item.', { path } );
}
// Normalize the root and path (if element was passed).
path = root.getPath().concat( path );
root = root.root;
/**
* Root of the position path.
*
* @readonly
* @member {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
* module:engine/model/position~Position#root
*/
this.root = root;
/**
* Position of the node in the tree. **Path contains offsets, not indexes.**
*
* Position can be placed before, after or in a {@link module:engine/model/node~Node node} if that node has
* {@link module:engine/model/node~Node#offsetSize} greater than `1`. Items in position path are
* {@link module:engine/model/node~Node#startOffset starting offsets} of position ancestors, starting from direct root children,
* down to the position offset in it's parent.
*
* ROOT
* |- P before: [ 0 ] after: [ 1 ]
* |- UL before: [ 1 ] after: [ 2 ]
* |- LI before: [ 1, 0 ] after: [ 1, 1 ]
* | |- foo before: [ 1, 0, 0 ] after: [ 1, 0, 3 ]
* |- LI before: [ 1, 1 ] after: [ 1, 2 ]
* |- bar before: [ 1, 1, 0 ] after: [ 1, 1, 3 ]
*
* `foo` and `bar` are representing {@link module:engine/model/text~Text text nodes}. Since text nodes has offset size
* greater than `1` you can place position offset between their start and end:
*
* ROOT
* |- P
* |- UL
* |- LI
* | |- f^o|o ^ has path: [ 1, 0, 1 ] | has path: [ 1, 0, 2 ]
* |- LI
* |- b^a|r ^ has path: [ 1, 1, 1 ] | has path: [ 1, 1, 2 ]
*
* @member {Array.<Number>} module:engine/model/position~Position#path
*/
this.path = path;
}
/**
* Offset at which this position is located in its {@link module:engine/model/position~Position#parent parent}. It is equal
* to the last item in position {@link module:engine/model/position~Position#path path}.
*
* @type {Number}
*/
get offset() {
return lodash_last( this.path );
}
/**
* @param {Number} newOffset
*/
set offset( newOffset ) {
this.path[ this.path.length - 1 ] = newOffset;
}
/**
* Parent element of this position.
*
* Keep in mind that `parent` value is calculated when the property is accessed.
* If {@link module:engine/model/position~Position#path position path}
* leads to a non-existing element, `parent` property will throw error.
*
* Also it is a good idea to cache `parent` property if it is used frequently in an algorithm (i.e. in a long loop).
*
* @readonly
* @type {module:engine/model/element~Element}
*/
get parent() {
let parent = this.root;
for ( let i = 0; i < this.path.length - 1; i++ ) {
parent = parent.getChild( parent.offsetToIndex( this.path[ i ] ) );
}
return parent;
}
/**
* Position {@link module:engine/model/position~Position#offset offset} converted to an index in position's parent node. It is
* equal to the {@link module:engine/model/node~Node#index index} of a node after this position. If position is placed
* in text node, position index is equal to the index of that text node.
*
* @readonly
* @type {Number}
*/
get index() {
return this.parent.offsetToIndex( this.offset );
}
/**
* Returns {@link module:engine/model/text~Text text node} instance in which this position is placed or `null` if this
* position is not in a text node.
*
* @readonly
* @type {module:engine/model/text~Text|null}
*/
get textNode() {
const node = this.parent.getChild( this.index );
return ( node instanceof text_Text && node.startOffset < this.offset ) ? node : null;
}
/**
* Node directly after this position or `null` if this position is in text node.
*
* @readonly
* @type {module:engine/model/node~Node|null}
*/
get nodeAfter() {
return this.textNode === null ? this.parent.getChild( this.index ) : null;
}
/**
* Node directly before this position or `null` if this position is in text node.
*
* @readonly
* @type {Node}
*/
get nodeBefore() {
return this.textNode === null ? this.parent.getChild( this.index - 1 ) : null;
}
/**
* Is `true` if position is at the beginning of its {@link module:engine/model/position~Position#parent parent}, `false` otherwise.
*
* @readonly
* @type {Boolean}
*/
get isAtStart() {
return this.offset === 0;
}
/**
* Is `true` if position is at the end of its {@link module:engine/model/position~Position#parent parent}, `false` otherwise.
*
* @readonly
* @type {Boolean}
*/
get isAtEnd() {
return this.offset == this.parent.maxOffset;
}
/**
* Checks whether this position is before or after given position.
*
* @param {module:engine/model/position~Position} otherPosition Position to compare with.
* @returns {module:engine/model/position~PositionRelation}
*/
compareWith( otherPosition ) {
if ( this.root != otherPosition.root ) {
return 'different';
}
const result = compareArrays( this.path, otherPosition.path );
switch ( result ) {
case 'same':
return 'same';
case 'prefix':
return 'before';
case 'extension':
return 'after';
default:
if ( this.path[ result ] < otherPosition.path[ result ] ) {
return 'before';
} else {
return 'after';
}
}
}
/**
* Gets the farthest position which matches the callback using
* {@link module:engine/model/treewalker~TreeWalker TreeWalker}.
*
* For example:
*
* getLastMatchingPosition( value => value.type == 'text' );
* // <paragraph>[]foo</paragraph> -> <paragraph>foo[]</paragraph>
*
* getLastMatchingPosition( value => value.type == 'text', { direction: 'backward' } );
* // <paragraph>foo[]</paragraph> -> <paragraph>[]foo</paragraph>
*
* getLastMatchingPosition( value => false );
* // Do not move the position.
*
* @param {Function} skip Callback function. Gets {@link module:engine/model/treewalker~TreeWalkerValue} and should
* return `true` if the value should be skipped or `false` if not.
* @param {Object} options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}.
*
* @returns {module:engine/model/position~Position} The position after the last item which matches the `skip` callback test.
*/
getLastMatchingPosition( skip, options = {} ) {
options.startPosition = this;
const treeWalker = new treewalker_TreeWalker( options );
treeWalker.skip( skip );
return treeWalker.position;
}
/**
* Returns a path to this position's parent. Parent path is equal to position {@link module:engine/model/position~Position#path path}
* but without the last item.
*
* This method returns the parent path even if the parent does not exists.
*
* @returns {Array.<Number>} Path to the parent.
*/
getParentPath() {
return this.path.slice( 0, -1 );
}
/**
* Returns ancestors array of this position, that is this position's parent and its ancestors.
*
* @returns {Array.<module:engine/model/item~Item>} Array with ancestors.
*/
getAncestors() {
if ( this.parent.is( 'documentFragment' ) ) {
return [ this.parent ];
} else {
return this.parent.getAncestors( { includeSelf: true } );
}
}
/**
* Returns the slice of two position {@link #path paths} which is identical. The {@link #root roots}
* of these two paths must be identical.
*
* @param {module:engine/model/position~Position} position The second position.
* @returns {Array.<Number>} The common path.
*/
getCommonPath( position ) {
if ( this.root != position.root ) {
return [];
}
// We find on which tree-level start and end have the lowest common ancestor
const cmp = compareArrays( this.path, position.path );
// If comparison returned string it means that arrays are same.
const diffAt = ( typeof cmp == 'string' ) ? Math.min( this.path.length, position.path.length ) : cmp;
return this.path.slice( 0, diffAt );
}
/**
* Returns an {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment}
* which is a common ancestor of both positions. The {@link #root roots} of these two positions must be identical.
*
* @param {module:engine/model/position~Position} position The second position.
* @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null}
*/
getCommonAncestor( position ) {
const ancestorsA = this.getAncestors();
const ancestorsB = position.getAncestors();
let i = 0;
while ( ancestorsA[ i ] == ancestorsB[ i ] && ancestorsA[ i ] ) {
i++;
}
return i === 0 ? null : ancestorsA[ i - 1 ];
}
/**
* Returns a new instance of `Position`, that has same {@link #parent parent} but it's offset
* is shifted by `shift` value (can be a negative value).
*
* @param {Number} shift Offset shift. Can be a negative value.
* @returns {module:engine/model/position~Position} Shifted position.
*/
getShiftedBy( shift ) {
const shifted = position_Position.createFromPosition( this );
const offset = shifted.offset + shift;
shifted.offset = offset < 0 ? 0 : offset;
return shifted;
}
/**
* Checks whether this position is after given position.
*
* @see module:engine/model/position~Position#isBefore
*
* @param {module:engine/model/position~Position} otherPosition Position to compare with.
* @returns {Boolean} True if this position is after given position.
*/
isAfter( otherPosition ) {
return this.compareWith( otherPosition ) == 'after';
}
/**
* Checks whether this position is before given position.
*
* **Note:** watch out when using negation of the value returned by this method, because the negation will also
* be `true` if positions are in different roots and you might not expect this. You should probably use
* `a.isAfter( b ) || a.isEqual( b )` or `!a.isBefore( p ) && a.root == b.root` in most scenarios. If your
* condition uses multiple `isAfter` and `isBefore` checks, build them so they do not use negated values, i.e.:
*
* if ( a.isBefore( b ) && c.isAfter( d ) ) {
* // do A.
* } else {
* // do B.
* }
*
* or, if you have only one if-branch:
*
* if ( !( a.isBefore( b ) && c.isAfter( d ) ) {
* // do B.
* }
*
* rather than:
*
* if ( !a.isBefore( b ) || && !c.isAfter( d ) ) {
* // do B.
* } else {
* // do A.
* }
*
* @param {module:engine/model/position~Position} otherPosition Position to compare with.
* @returns {Boolean} True if this position is before given position.
*/
isBefore( otherPosition ) {
return this.compareWith( otherPosition ) == 'before';
}
/**
* Checks whether this position is equal to given position.
*
* @param {module:engine/model/position~Position} otherPosition Position to compare with.
* @returns {Boolean} True if positions are same.
*/
isEqual( otherPosition ) {
return this.compareWith( otherPosition ) == 'same';
}
/**
* Checks whether this position is touching given position. Positions touch when there are no text nodes
* or empty nodes in a range between them. Technically, those positions are not equal but in many cases
* they are very similar or even indistinguishable.
*
* **Note:** this method traverses model document so it can be only used when range is up-to-date with model document.
*
* @param {module:engine/model/position~Position} otherPosition Position to compare with.
* @returns {Boolean} True if positions touch.
*/
isTouching( otherPosition ) {
let left = null;
let right = null;
const compare = this.compareWith( otherPosition );
switch ( compare ) {
case 'same':
return true;
case 'before':
left = position_Position.createFromPosition( this );
right = position_Position.createFromPosition( otherPosition );
break;
case 'after':
left = position_Position.createFromPosition( otherPosition );
right = position_Position.createFromPosition( this );
break;
default:
return false;
}
// Cached for optimization purposes.
let leftParent = left.parent;
while ( left.path.length + right.path.length ) {
if ( left.isEqual( right ) ) {
return true;
}
if ( left.path.length > right.path.length ) {
if ( left.offset !== leftParent.maxOffset ) {
return false;
}
left.path = left.path.slice( 0, -1 );
leftParent = leftParent.parent;
left.offset++;
} else {
if ( right.offset !== 0 ) {
return false;
}
right.path = right.path.slice( 0, -1 );
}
}
}
/**
* Returns a copy of this position that is updated by removing `howMany` nodes starting from `deletePosition`.
* It may happen that this position is in a removed node. If that is the case, `null` is returned instead.
*
* @protected
* @param {module:engine/model/position~Position} deletePosition Position before the first removed node.
* @param {Number} howMany How many nodes are removed.
* @returns {module:engine/model/position~Position|null} Transformed position or `null`.
*/
_getTransformedByDeletion( deletePosition, howMany ) {
const transformed = position_Position.createFromPosition( this );
// This position can't be affected if deletion was in a different root.
if ( this.root != deletePosition.root ) {
return transformed;
}
if ( compareArrays( deletePosition.getParentPath(), this.getParentPath() ) == 'same' ) {
// If nodes are removed from the node that is pointed by this position...
if ( deletePosition.offset < this.offset ) {
// And are removed from before an offset of that position...
if ( deletePosition.offset + howMany > this.offset ) {
// Position is in removed range, it's no longer in the tree.
return null;
} else {
// Decrement the offset accordingly.
transformed.offset -= howMany;
}
}
} else if ( compareArrays( deletePosition.getParentPath(), this.getParentPath() ) == 'prefix' ) {
// If nodes are removed from a node that is on a path to this position...
const i = deletePosition.path.length - 1;
if ( deletePosition.offset <= this.path[ i ] ) {
// And are removed from before next node of that path...
if ( deletePosition.offset + howMany > this.path[ i ] ) {
// If the next node of that path is removed return null
// because the node containing this position got removed.
return null;
} else {
// Otherwise, decrement index on that path.
transformed.path[ i ] -= howMany;
}
}
}
return transformed;
}
/**
* Returns a copy of this position that is updated by inserting `howMany` nodes at `insertPosition`.
*
* @protected
* @param {module:engine/model/position~Position} insertPosition Position where nodes are inserted.
* @param {Number} howMany How many nodes are inserted.
* @param {Boolean} insertBefore Flag indicating whether nodes are inserted before or after `insertPosition`.
* This is important only when `insertPosition` and this position are same. If that is the case and the flag is
* set to `true`, this position will get transformed. If the flag is set to `false`, it won't.
* @returns {module:engine/model/position~Position} Transformed position.
*/
_getTransformedByInsertion( insertPosition, howMany, insertBefore ) {
const transformed = position_Position.createFromPosition( this );
// This position can't be affected if insertion was in a different root.
if ( this.root != insertPosition.root ) {
return transformed;
}
if ( compareArrays( insertPosition.getParentPath(), this.getParentPath() ) == 'same' ) {
// If nodes are inserted in the node that is pointed by this position...
if ( insertPosition.offset < this.offset || ( insertPosition.offset == this.offset && insertBefore ) ) {
// And are inserted before an offset of that position...
// "Push" this positions offset.
transformed.offset += howMany;
}
} else if ( compareArrays( insertPosition.getParentPath(), this.getParentPath() ) == 'prefix' ) {
// If nodes are inserted in a node that is on a path to this position...
const i = insertPosition.path.length - 1;
if ( insertPosition.offset <= this.path[ i ] ) {
// And are inserted before next node of that path...
// "Push" the index on that path.
transformed.path[ i ] += howMany;
}
}
return transformed;
}
/**
* Returns a copy of this position that is updated by moving `howMany` nodes from `sourcePosition` to `targetPosition`.
*
* @protected
* @param {module:engine/model/position~Position} sourcePosition Position before the first element to move.
* @param {module:engine/model/position~Position} targetPosition Position where moved elements will be inserted.
* @param {Number} howMany How many consecutive nodes to move, starting from `sourcePosition`.
* @param {Boolean} insertBefore Flag indicating whether moved nodes are pasted before or after `insertPosition`.
* This is important only when `targetPosition` and this position are same. If that is the case and the flag is
* set to `true`, this position will get transformed by range insertion. If the flag is set to `false`, it won't.
* @param {Boolean} [sticky] Flag indicating whether this position "sticks" to range, that is if it should be moved
* with the moved range if it is equal to one of range's boundaries.
* @returns {module:engine/model/position~Position} Transformed position.
*/
_getTransformedByMove( sourcePosition, targetPosition, howMany, insertBefore, sticky ) {
// Moving a range removes nodes from their original position. We acknowledge this by proper transformation.
let transformed = this._getTransformedByDeletion( sourcePosition, howMany );
// Then we update target position, as it could be affected by nodes removal too.
targetPosition = targetPosition._getTransformedByDeletion( sourcePosition, howMany );
if ( transformed === null || ( sticky && transformed.isEqual( sourcePosition ) ) ) {
// This position is inside moved range (or sticks to it).
// In this case, we calculate a combination of this position, move source position and target position.
transformed = this._getCombined( sourcePosition, targetPosition );
} else {
// This position is not inside a removed range.
// In next step, we simply reflect inserting `howMany` nodes, which might further affect the position.
transformed = transformed._getTransformedByInsertion( targetPosition, howMany, insertBefore );
}
return transformed;
}
/**
* Returns a new position that is a combination of this position and given positions.
*
* The combined position is a copy of this position transformed by moving a range starting at `source` position
* to the `target` position. It is expected that this position is inside the moved range.
*
* Example:
*
* let original = new Position( root, [ 2, 3, 1 ] );
* let source = new Position( root, [ 2, 2 ] );
* let target = new Position( otherRoot, [ 1, 1, 3 ] );
* original._getCombined( source, target ); // path is [ 1, 1, 4, 1 ], root is `otherRoot`
*
* Explanation:
*
* We have a position `[ 2, 3, 1 ]` and move some nodes from `[ 2, 2 ]` to `[ 1, 1, 3 ]`. The original position
* was inside moved nodes and now should point to the new place. The moved nodes will be after
* positions `[ 1, 1, 3 ]`, `[ 1, 1, 4 ]`, `[ 1, 1, 5 ]`. Since our position was in the second moved node,
* the transformed position will be in a sub-tree of a node at `[ 1, 1, 4 ]`. Looking at original path, we
* took care of `[ 2, 3 ]` part of it. Now we have to add the rest of the original path to the transformed path.
* Finally, the transformed position will point to `[ 1, 1, 4, 1 ]`.
*
* @protected
* @param {module:engine/model/position~Position} source Beginning of the moved range.
* @param {module:engine/model/position~Position} target Position where the range is moved.
* @returns {module:engine/model/position~Position} Combined position.
*/
_getCombined( source, target ) {
const i = source.path.length - 1;
// The first part of a path to combined position is a path to the place where nodes were moved.
const combined = position_Position.createFromPosition( target );
// Then we have to update the rest of the path.
// Fix the offset because this position might be after `from` position and we have to reflect that.
combined.offset = combined.offset + this.path[ i ] - source.offset;
// Then, add the rest of the path.
// If this position is at the same level as `from` position nothing will get added.
combined.path = combined.path.concat( this.path.slice( i + 1 ) );
return combined;
}
/**
* Creates position at the given location. The location can be specified as:
*
* * a {@link module:engine/model/position~Position position},
* * parent element and offset (offset defaults to `0`),
* * parent element and `'end'` (sets position at the end of that element),
* * {@link module:engine/model/item~Item model item} and `'before'` or `'after'` (sets position before or after given model item).
*
* This method is a shortcut to other constructors such as:
*
* * {@link module:engine/model/position~Position.createBefore},
* * {@link module:engine/model/position~Position.createAfter},
* * {@link module:engine/model/position~Position.createFromParentAndOffset},
* * {@link module:engine/model/position~Position.createFromPosition}.
*
* @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
* @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
* first parameter is a {@link module:engine/model/item~Item model item}.
*/
static createAt( itemOrPosition, offset ) {
if ( itemOrPosition instanceof position_Position ) {
return this.createFromPosition( itemOrPosition );
} else {
const node = itemOrPosition;
if ( offset == 'end' ) {
offset = node.maxOffset;
} else if ( offset == 'before' ) {
return this.createBefore( node );
} else if ( offset == 'after' ) {
return this.createAfter( node );
} else if ( !offset ) {
offset = 0;
}
return this.createFromParentAndOffset( node, offset );
}
}
/**
* Creates a new position, after given {@link module:engine/model/item~Item model item}.
*
* @param {module:engine/model/item~Item} item Item after which the position should be placed.
* @returns {module:engine/model/position~Position}
*/
static createAfter( item ) {
if ( !item.parent ) {
/**
* You can not make a position after a root element.
*
* @error model-position-after-root
* @param {module:engine/model/item~Item} root
*/
throw new CKEditorError( 'model-position-after-root: You cannot make a position after root.', { root: item } );
}
return this.createFromParentAndOffset( item.parent, item.endOffset );
}
/**
* Creates a new position, before the given {@link module:engine/model/item~Item model item}.
*
* @param {module:engine/model/item~Item} item Item before which the position should be placed.
* @returns {module:engine/model/position~Position}
*/
static createBefore( item ) {
if ( !item.parent ) {
/**
* You can not make a position before a root element.
*
* @error model-position-before-root
* @param {module:engine/model/item~Item} root
*/
throw new CKEditorError( 'model-position-before-root: You cannot make a position before root.', { root: item } );
}
return this.createFromParentAndOffset( item.parent, item.startOffset );
}
/**
* Creates a new position from the parent element and an offset in that element.
*
* @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent Position's parent.
* @param {Number} offset Position's offset.
* @returns {module:engine/model/position~Position}
*/
static createFromParentAndOffset( parent, offset ) {
if ( !parent.is( 'element' ) && !parent.is( 'documentFragment' ) ) {
/**
* Position parent have to be a model element or model document fragment.
*
* @error model-position-parent-incorrect
*/
throw new CKEditorError( 'model-position-parent-incorrect: Position parent have to be a element or document fragment.' );
}
const path = parent.getPath();
path.push( offset );
return new this( parent.root, path );
}
/**
* Creates a new position, which is equal to passed position.
*
* @param {module:engine/model/position~Position} position Position to be cloned.
* @returns {module:engine/model/position~Position}
*/
static createFromPosition( position ) {
return new this( position.root, position.path.slice() );
}
/**
* Creates a `Position` instance from given plain object (i.e. parsed JSON string).
*
* @param {Object} json Plain object to be converted to `Position`.
* @returns {module:engine/model/position~Position} `Position` instance created using given plain object.
*/
static fromJSON( json, doc ) {
if ( json.root === '$graveyard' ) {
return new position_Position( doc.graveyard, json.path );
}
if ( !doc.hasRoot( json.root ) ) {
/**
* Cannot create position for document. Root with specified name does not exist.
*
* @error model-position-fromjson-no-root
* @param {String} rootName
*/
throw new CKEditorError(
'model-position-fromjson-no-root: Cannot create position for document. Root with specified name does not exist.',
{ rootName: json.root }
);
}
return new position_Position( doc.getRoot( json.root ), json.path );
}
}
/**
* A flag indicating whether this position is `'before'` or `'after'` or `'same'` as given position.
* If positions are in different roots `'different'` flag is returned.
*
* @typedef {String} module:engine/model/position~PositionRelation
*/
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/range.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/range
*/
/**
* Range class. Range is iterable.
*/
class range_Range {
/**
* Creates a range spanning from `start` position to `end` position.
*
* **Note:** Constructor creates it's own {@link module:engine/model/position~Position Position} instances basing on passed values.
*
* @param {module:engine/model/position~Position} start Start position.
* @param {module:engine/model/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
*/
constructor( start, end = null ) {
/**
* Start position.
*
* @readonly
* @member {module:engine/model/position~Position}
*/
this.start = position_Position.createFromPosition( start );
/**
* End position.
*
* @readonly
* @member {module:engine/model/position~Position}
*/
this.end = end ? position_Position.createFromPosition( end ) : position_Position.createFromPosition( start );
}
/**
* Returns an iterator that iterates over all {@link module:engine/model/item~Item items} that are in this range and returns
* them together with additional information like length or {@link module:engine/model/position~Position positions},
* grouped as {@link module:engine/model/treewalker~TreeWalkerValue}.
* It iterates over all {@link module:engine/model/textproxy~TextProxy text contents} that are inside the range
* and all the {@link module:engine/model/element~Element}s that are entered into when iterating over this range.
*
* This iterator uses {@link module:engine/model/treewalker~TreeWalker} with `boundaries` set to this range
* and `ignoreElementEnd` option set to `true`.
*
* @returns {Iterable.<module:engine/model/treewalker~TreeWalkerValue>}
*/
* [ Symbol.iterator ]() {
yield* new treewalker_TreeWalker( { boundaries: this, ignoreElementEnd: true } );
}
/**
* Returns whether the range is collapsed, that is if {@link #start} and
* {@link #end} positions are equal.
*
* @type {Boolean}
*/
get isCollapsed() {
return this.start.isEqual( this.end );
}
/**
* Returns whether this range is flat, that is if {@link #start} position and
* {@link #end} position are in the same {@link module:engine/model/position~Position#parent}.
*
* @type {Boolean}
*/
get isFlat() {
return this.start.parent === this.end.parent;
}
/**
* Range root element.
*
* @type {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
*/
get root() {
return this.start.root;
}
/**
* Checks whether this range contains given {@link module:engine/model/position~Position position}.
*
* @param {module:engine/model/position~Position} position Position to check.
* @returns {Boolean} `true` if given {@link module:engine/model/position~Position position} is contained
* in this range,`false` otherwise.
*/
containsPosition( position ) {
return position.isAfter( this.start ) && position.isBefore( this.end );
}
/**
* Checks whether this range contains given {@link ~Range range}.
*
* @param {module:engine/model/range~Range} otherRange Range to check.
* @param {Boolean} [loose=false] Whether the check is loose or strict. If the check is strict (`false`), compared range cannot
* start or end at the same position as this range boundaries. If the check is loose (`true`), compared range can start, end or
* even be equal to this range. Note that collapsed ranges are always compared in strict mode.
* @returns {Boolean} `true` if given {@link ~Range range} boundaries are contained by this range, `false` otherwise.
*/
containsRange( otherRange, loose = false ) {
if ( otherRange.isCollapsed ) {
loose = false;
}
const containsStart = this.containsPosition( otherRange.start ) || ( loose && this.start.isEqual( otherRange.start ) );
const containsEnd = this.containsPosition( otherRange.end ) || ( loose && this.end.isEqual( otherRange.end ) );
return containsStart && containsEnd;
}
/**
* Checks whether given {@link module:engine/model/item~Item} is inside this range.
*
* @param {module:engine/model/item~Item} item Model item to check.
*/
containsItem( item ) {
const pos = position_Position.createBefore( item );
return this.containsPosition( pos ) || this.start.isEqual( pos );
}
/**
* Two ranges are equal if their {@link #start} and {@link #end} positions are equal.
*
* @param {module:engine/model/range~Range} otherRange Range to compare with.
* @returns {Boolean} `true` if ranges are equal, `false` otherwise.
*/
isEqual( otherRange ) {
return this.start.isEqual( otherRange.start ) && this.end.isEqual( otherRange.end );
}
/**
* Checks and returns whether this range intersects with given range.
*
* @param {module:engine/model/range~Range} otherRange Range to compare with.
* @returns {Boolean} `true` if ranges intersect, `false` otherwise.
*/
isIntersecting( otherRange ) {
return this.start.isBefore( otherRange.end ) && this.end.isAfter( otherRange.start );
}
/**
* Computes which part(s) of this {@link ~Range range} is not a part of given {@link ~Range range}.
* Returned array contains zero, one or two {@link ~Range ranges}.
*
* Examples:
*
* let range = new Range( new Position( root, [ 2, 7 ] ), new Position( root, [ 4, 0, 1 ] ) );
* let otherRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 5 ] ) );
* let transformed = range.getDifference( otherRange );
* // transformed array has no ranges because `otherRange` contains `range`
*
* otherRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 3 ] ) );
* transformed = range.getDifference( otherRange );
* // transformed array has one range: from [ 3 ] to [ 4, 0, 1 ]
*
* otherRange = new Range( new Position( root, [ 3 ] ), new Position( root, [ 4 ] ) );
* transformed = range.getDifference( otherRange );
* // transformed array has two ranges: from [ 2, 7 ] to [ 3 ] and from [ 4 ] to [ 4, 0, 1 ]
*
* @param {module:engine/model/range~Range} otherRange Range to differentiate against.
* @returns {Array.<module:engine/model/range~Range>} The difference between ranges.
*/
getDifference( otherRange ) {
const ranges = [];
if ( this.isIntersecting( otherRange ) ) {
// Ranges intersect.
if ( this.containsPosition( otherRange.start ) ) {
// Given range start is inside this range. This means that we have to
// add shrunken range - from the start to the middle of this range.
ranges.push( new range_Range( this.start, otherRange.start ) );
}
if ( this.containsPosition( otherRange.end ) ) {
// Given range end is inside this range. This means that we have to
// add shrunken range - from the middle of this range to the end.
ranges.push( new range_Range( otherRange.end, this.end ) );
}
} else {
// Ranges do not intersect, return the original range.
ranges.push( range_Range.createFromRange( this ) );
}
return ranges;
}
/**
* Returns an intersection of this {@link ~Range range} and given {@link ~Range range}.
* Intersection is a common part of both of those ranges. If ranges has no common part, returns `null`.
*
* Examples:
*
* let range = new Range( new Position( root, [ 2, 7 ] ), new Position( root, [ 4, 0, 1 ] ) );
* let otherRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
* let transformed = range.getIntersection( otherRange ); // null - ranges have no common part
*
* otherRange = new Range( new Position( root, [ 3 ] ), new Position( root, [ 5 ] ) );
* transformed = range.getIntersection( otherRange ); // range from [ 3 ] to [ 4, 0, 1 ]
*
* @param {module:engine/model/range~Range} otherRange Range to check for intersection.
* @returns {module:engine/model/range~Range|null} A common part of given ranges or `null` if ranges have no common part.
*/
getIntersection( otherRange ) {
if ( this.isIntersecting( otherRange ) ) {
// Ranges intersect, so a common range will be returned.
// At most, it will be same as this range.
let commonRangeStart = this.start;
let commonRangeEnd = this.end;
if ( this.containsPosition( otherRange.start ) ) {
// Given range start is inside this range. This means thaNt we have to
// shrink common range to the given range start.
commonRangeStart = otherRange.start;
}
if ( this.containsPosition( otherRange.end ) ) {
// Given range end is inside this range. This means that we have to
// shrink common range to the given range end.
commonRangeEnd = otherRange.end;
}
return new range_Range( commonRangeStart, commonRangeEnd );
}
// Ranges do not intersect, so they do not have common part.
return null;
}
/**
* Computes and returns the smallest set of {@link #isFlat flat} ranges, that covers this range in whole.
*
* See an example of a model structure (`[` and `]` are range boundaries):
*
* root root
* |- element DIV DIV P2 P3 DIV
* | |- element H H P1 f o o b a r H P4
* | | |- "fir[st" fir[st lorem se]cond ipsum
* | |- element P1
* | | |- "lorem" ||
* |- element P2 ||
* | |- "foo" VV
* |- element P3
* | |- "bar" root
* |- element DIV DIV [P2 P3] DIV
* | |- element H H [P1] f o o b a r H P4
* | | |- "se]cond" fir[st] lorem [se]cond ipsum
* | |- element P4
* | | |- "ipsum"
*
* As it can be seen, letters contained in the range are: `stloremfoobarse`, spread across different parents.
* We are looking for minimal set of flat ranges that contains the same nodes.
*
* Minimal flat ranges for above range `( [ 0, 0, 3 ], [ 3, 0, 2 ] )` will be:
*
* ( [ 0, 0, 3 ], [ 0, 0, 5 ] ) = "st"
* ( [ 0, 1 ], [ 0, 2 ] ) = element P1 ("lorem")
* ( [ 1 ], [ 3 ] ) = element P2, element P3 ("foobar")
* ( [ 3, 0, 0 ], [ 3, 0, 2 ] ) = "se"
*
* **Note:** if an {@link module:engine/model/element~Element element} is not wholly contained in this range, it won't be returned
* in any of the returned flat ranges. See in the example how `H` elements at the beginning and at the end of the range
* were omitted. Only their parts that were wholly in the range were returned.
*
* **Note:** this method is not returning flat ranges that contain no nodes.
*
* @returns {Array.<module:engine/model/range~Range>} Array of flat ranges covering this range.
*/
getMinimalFlatRanges() {
const ranges = [];
const diffAt = this.start.getCommonPath( this.end ).length;
const pos = position_Position.createFromPosition( this.start );
let posParent = pos.parent;
// Go up.
while ( pos.path.length > diffAt + 1 ) {
const howMany = posParent.maxOffset - pos.offset;
if ( howMany !== 0 ) {
ranges.push( new range_Range( pos, pos.getShiftedBy( howMany ) ) );
}
pos.path = pos.path.slice( 0, -1 );
pos.offset++;
posParent = posParent.parent;
}
// Go down.
while ( pos.path.length <= this.end.path.length ) {
const offset = this.end.path[ pos.path.length - 1 ];
const howMany = offset - pos.offset;
if ( howMany !== 0 ) {
ranges.push( new range_Range( pos, pos.getShiftedBy( howMany ) ) );
}
pos.offset = offset;
pos.path.push( 0 );
}
return ranges;
}
/**
* Creates a {@link module:engine/model/treewalker~TreeWalker TreeWalker} instance with this range as a boundary.
*
* @param {Object} options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}.
* @param {module:engine/model/position~Position} [options.startPosition]
* @param {Boolean} [options.singleCharacters=false]
* @param {Boolean} [options.shallow=false]
* @param {Boolean} [options.ignoreElementEnd=false]
*/
getWalker( options = {} ) {
options.boundaries = this;
return new treewalker_TreeWalker( options );
}
/**
* Returns an iterator that iterates over all {@link module:engine/model/item~Item items} that are in this range and returns
* them.
*
* This method uses {@link module:engine/model/treewalker~TreeWalker} with `boundaries` set to this range and `ignoreElementEnd` option
* set to `true`. However it returns only {@link module:engine/model/item~Item model items},
* not {@link module:engine/model/treewalker~TreeWalkerValue}.
*
* You may specify additional options for the tree walker. See {@link module:engine/model/treewalker~TreeWalker} for
* a full list of available options.
*
* @method getItems
* @param {Object} options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}.
* @returns {Iterable.<module:engine/model/item~Item>}
*/
* getItems( options = {} ) {
options.boundaries = this;
options.ignoreElementEnd = true;
const treeWalker = new treewalker_TreeWalker( options );
for ( const value of treeWalker ) {
yield value.item;
}
}
/**
* Returns an iterator that iterates over all {@link module:engine/model/position~Position positions} that are boundaries or
* contained in this range.
*
* This method uses {@link module:engine/model/treewalker~TreeWalker} with `boundaries` set to this range. However it returns only
* {@link module:engine/model/position~Position positions}, not {@link module:engine/model/treewalker~TreeWalkerValue}.
*
* You may specify additional options for the tree walker. See {@link module:engine/model/treewalker~TreeWalker} for
* a full list of available options.
*
* @param {Object} options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}.
* @returns {Iterable.<module:engine/model/position~Position>}
*/
* getPositions( options = {} ) {
options.boundaries = this;
const treeWalker = new treewalker_TreeWalker( options );
yield treeWalker.position;
for ( const value of treeWalker ) {
yield value.nextPosition;
}
}
/**
* Returns a range that is a result of transforming this range by given `delta`.
*
* **Note:** transformation may break one range into multiple ranges (e.g. when a part of the range is
* moved to a different part of document tree). For this reason, an array is returned by this method and it
* may contain one or more `Range` instances.
*
* @param {module:engine/model/delta/delta~Delta} delta Delta to transform range by.
* @returns {Array.<module:engine/model/range~Range>} Range which is the result of transformation.
*/
getTransformedByDelta( delta ) {
const ranges = [ range_Range.createFromRange( this ) ];
// Operation types that a range can be transformed by.
const supportedTypes = new Set( [ 'insert', 'move', 'remove', 'reinsert' ] );
for ( const operation of delta.operations ) {
if ( supportedTypes.has( operation.type ) ) {
for ( let i = 0; i < ranges.length; i++ ) {
const result = ranges[ i ]._getTransformedByDocumentChange(
operation.type,
delta.type,
operation.targetPosition || operation.position,
operation.howMany || operation.nodes.maxOffset,
operation.sourcePosition
);
ranges.splice( i, 1, ...result );
i += result.length - 1;
}
}
}
return ranges;
}
/**
* Returns a range that is a result of transforming this range by multiple `deltas`.
*
* **Note:** transformation may break one range into multiple ranges (e.g. when a part of the range is
* moved to a different part of document tree). For this reason, an array is returned by this method and it
* may contain one or more `Range` instances.
*
* @param {Iterable.<module:engine/model/delta/delta~Delta>} deltas Deltas to transform the range by.
* @returns {Array.<module:engine/model/range~Range>} Range which is the result of transformation.
*/
getTransformedByDeltas( deltas ) {
const ranges = [ range_Range.createFromRange( this ) ];
for ( const delta of deltas ) {
for ( let i = 0; i < ranges.length; i++ ) {
const result = ranges[ i ].getTransformedByDelta( delta );
ranges.splice( i, 1, ...result );
i += result.length - 1;
}
}
// It may happen that a range is split into two, and then the part of second "piece" is moved into first
// "piece". In this case we will have incorrect third range, which should not be included in the result --
// because it is already included in the first "piece". In this loop we are looking for all such ranges that
// are inside other ranges and we simply remove them.
for ( let i = 0; i < ranges.length; i++ ) {
const range = ranges[ i ];
for ( let j = i + 1; j < ranges.length; j++ ) {
const next = ranges[ j ];
if ( range.containsRange( next ) || next.containsRange( range ) || range.isEqual( next ) ) {
ranges.splice( j, 1 );
}
}
}
return ranges;
}
/**
* Returns an {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment}
* which is a common ancestor of the range's both ends (in which the entire range is contained).
*
* @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null}
*/
getCommonAncestor() {
return this.start.getCommonAncestor( this.end );
}
/**
* Returns a range that is a result of transforming this range by a change in the model document.
*
* @protected
* @param {'insert'|'move'|'remove'|'reinsert'} type Change type.
* @param {String} deltaType Type of delta that introduced the change.
* @param {module:engine/model/position~Position} targetPosition Position before the first changed node.
* @param {Number} howMany How many nodes has been changed.
* @param {module:engine/model/position~Position} sourcePosition Source position of changes.
* @returns {Array.<module:engine/model/range~Range>}
*/
_getTransformedByDocumentChange( type, deltaType, targetPosition, howMany, sourcePosition ) {
if ( type == 'insert' ) {
return this._getTransformedByInsertion( targetPosition, howMany, false, false );
} else {
const sourceRange = range_Range.createFromPositionAndShift( sourcePosition, howMany );
// Edge case for merge delta.
if (
deltaType == 'merge' &&
this.isCollapsed &&
( this.start.isEqual( sourceRange.start ) || this.start.isEqual( sourceRange.end ) )
) {
// Collapsed range is in merged element, at the beginning or at the end of it.
// Without fix, the range would end up in the graveyard, together with removed element.
// <p>foo</p><p>[]bar</p> -> <p>foobar</p><p>[]</p> -> <p>foobar</p> -> <p>foo[]bar</p>
// <p>foo</p><p>bar[]</p> -> <p>foobar</p><p>[]</p> -> <p>foobar</p> -> <p>foobar[]</p>
//
// In most cases, `sourceRange.start.offset` for merge delta's move operation would be 0,
// so this formula might look overcomplicated.
// However in some scenarios, after operational transformation, move operation might not
// in fact start from 0 and we need to properly count new offset.
// https://github.com/ckeditor/ckeditor5-engine/pull/1133#issuecomment-329080668.
const offset = this.start.offset - sourceRange.start.offset;
return [ new range_Range( targetPosition.getShiftedBy( offset ) ) ];
}
//
// Edge case for split delta.
//
if ( deltaType == 'split' && this.isCollapsed && this.end.isEqual( sourceRange.end ) ) {
// Collapsed range is at the end of split element.
// Without fix, the range would end up at the end of split (old) element instead of at the end of new element.
// That would happen because this range is not technically inside moved range. Last step below shows the fix.
// <p>foobar[]</p> -> <p>foobar[]</p><p></p> -> <p>foo[]</p><p>bar</p> -> <p>foo</p><p>bar[]</p>
return [ new range_Range( targetPosition.getShiftedBy( howMany ) ) ];
}
//
// Other edge cases:
//
// In all examples `[]` is `this` and `{}` is `sourceRange`, while `^` is move target position.
//
// Example:
// <p>xx</p>^<w>{<p>a[b</p>}</w><p>c]d</p> --> <p>xx</p><p>a[b</p><w></w><p>c]d</p>
// ^<p>xx</p><w>{<p>a[b</p>}</w><p>c]d</p> --> <p>a[b</p><p>xx</p><w></w><p>c]d</p> // Note <p>xx</p> inclusion.
// <w>{<p>a[b</p>}</w>^<p>c]d</p> --> <w></w><p>a[b</p><p>c]d</p>
if (
( sourceRange.containsPosition( this.start ) || sourceRange.start.isEqual( this.start ) ) &&
this.containsPosition( sourceRange.end ) &&
this.end.isAfter( targetPosition )
) {
const start = this.start._getCombined(
sourcePosition,
targetPosition._getTransformedByDeletion( sourcePosition, howMany )
);
const end = this.end._getTransformedByMove( sourcePosition, targetPosition, howMany, false, false );
return [ new range_Range( start, end ) ];
}
// Example:
// <p>c[d</p><w>{<p>a]b</p>}</w>^<p>xx</p> --> <p>c[d</p><w></w><p>a]b</p><p>xx</p>
// <p>c[d</p><w>{<p>a]b</p>}</w><p>xx</p>^ --> <p>c[d</p><w></w><p>xx</p><p>a]b</p> // Note <p>xx</p> inclusion.
// <p>c[d</p>^<w>{<p>a]b</p>}</w> --> <p>c[d</p><p>a]b</p><w></w>
if (
( sourceRange.containsPosition( this.end ) || sourceRange.end.isEqual( this.end ) ) &&
this.containsPosition( sourceRange.start ) &&
this.start.isBefore( targetPosition )
) {
const start = this.start._getTransformedByMove(
sourcePosition,
targetPosition,
howMany,
true,
false
);
const end = this.end._getCombined(
sourcePosition,
targetPosition._getTransformedByDeletion( sourcePosition, howMany )
);
return [ new range_Range( start, end ) ];
}
return this._getTransformedByMove( sourcePosition, targetPosition, howMany );
}
}
/**
* Returns an array containing one or two {@link ~Range ranges} that are a result of transforming this
* {@link ~Range range} by inserting `howMany` nodes at `insertPosition`. Two {@link ~Range ranges} are
* returned if the insertion was inside this {@link ~Range range} and `spread` is set to `true`.
*
* Examples:
*
* let range = new Range( new Position( root, [ 2, 7 ] ), new Position( root, [ 4, 0, 1 ] ) );
* let transformed = range._getTransformedByInsertion( new Position( root, [ 1 ] ), 2 );
* // transformed array has one range from [ 4, 7 ] to [ 6, 0, 1 ]
*
* transformed = range._getTransformedByInsertion( new Position( root, [ 4, 0, 0 ] ), 4 );
* // transformed array has one range from [ 2, 7 ] to [ 4, 0, 5 ]
*
* transformed = range._getTransformedByInsertion( new Position( root, [ 3, 2 ] ), 4 );
* // transformed array has one range, which is equal to original range
*
* transformed = range._getTransformedByInsertion( new Position( root, [ 3, 2 ] ), 4, true );
* // transformed array has two ranges: from [ 2, 7 ] to [ 3, 2 ] and from [ 3, 6 ] to [ 4, 0, 1 ]
*
* transformed = range._getTransformedByInsertion( new Position( root, [ 4, 0, 1 ] ), 4, false, false );
* // transformed array has one range which is equal to original range because insertion is after the range boundary
*
* transformed = range._getTransformedByInsertion( new Position( root, [ 4, 0, 1 ] ), 4, false, true );
* // transformed array has one range: from [ 2, 7 ] to [ 4, 0, 5 ] because range was expanded
*
* @protected
* @param {module:engine/model/position~Position} insertPosition Position where nodes are inserted.
* @param {Number} howMany How many nodes are inserted.
* @param {Boolean} [spread] Flag indicating whether this {~Range range} should be spread if insertion
* was inside the range. Defaults to `false`.
* @param {Boolean} [isSticky] Flag indicating whether insertion should expand a range if it is in a place of
* range boundary. Defaults to `false`.
* @returns {Array.<module:engine/model/range~Range>} Result of the transformation.
*/
_getTransformedByInsertion( insertPosition, howMany, spread = false, isSticky = false ) {
if ( spread && this.containsPosition( insertPosition ) ) {
// Range has to be spread. The first part is from original start to the spread point.
// The other part is from spread point to the original end, but transformed by
// insertion to reflect insertion changes.
return [
new range_Range( this.start, insertPosition ),
new range_Range(
insertPosition._getTransformedByInsertion( insertPosition, howMany, true ),
this.end._getTransformedByInsertion( insertPosition, howMany, this.isCollapsed )
)
];
} else {
const range = range_Range.createFromRange( this );
const insertBeforeStart = !isSticky;
const insertBeforeEnd = range.isCollapsed ? true : isSticky;
range.start = range.start._getTransformedByInsertion( insertPosition, howMany, insertBeforeStart );
range.end = range.end._getTransformedByInsertion( insertPosition, howMany, insertBeforeEnd );
return [ range ];
}
}
/**
* Returns an array containing {@link ~Range ranges} that are a result of transforming this
* {@link ~Range range} by moving `howMany` nodes from `sourcePosition` to `targetPosition`.
*
* @protected
* @param {module:engine/model/position~Position} sourcePosition Position from which nodes are moved.
* @param {module:engine/model/position~Position} targetPosition Position to where nodes are moved.
* @param {Number} howMany How many nodes are moved.
* @returns {Array.<module:engine/model/range~Range>} Result of the transformation.
*/
_getTransformedByMove( sourcePosition, targetPosition, howMany ) {
if ( this.isCollapsed ) {
const newPos = this.start._getTransformedByMove( sourcePosition, targetPosition, howMany, true, false );
return [ new range_Range( newPos ) ];
}
let result;
const moveRange = new range_Range( sourcePosition, sourcePosition.getShiftedBy( howMany ) );
const differenceSet = this.getDifference( moveRange );
let difference = null;
const common = this.getIntersection( moveRange );
if ( differenceSet.length == 1 ) {
// `moveRange` and this range may intersect.
difference = new range_Range(
differenceSet[ 0 ].start._getTransformedByDeletion( sourcePosition, howMany ),
differenceSet[ 0 ].end._getTransformedByDeletion( sourcePosition, howMany )
);
} else if ( differenceSet.length == 2 ) {
// `moveRange` is inside this range.
difference = new range_Range(
this.start,
this.end._getTransformedByDeletion( sourcePosition, howMany )
);
} // else, `moveRange` contains this range.
const insertPosition = targetPosition._getTransformedByDeletion( sourcePosition, howMany );
if ( difference ) {
result = difference._getTransformedByInsertion( insertPosition, howMany, common !== null );
} else {
result = [];
}
if ( common ) {
result.push( new range_Range(
common.start._getCombined( moveRange.start, insertPosition ),
common.end._getCombined( moveRange.start, insertPosition )
) );
}
return result;
}
/**
* Creates a new range, spreading from specified {@link module:engine/model/position~Position position} to a position moved by
* given `shift`. If `shift` is a negative value, shifted position is treated as the beginning of the range.
*
* @param {module:engine/model/position~Position} position Beginning of the range.
* @param {Number} shift How long the range should be.
* @returns {module:engine/model/range~Range}
*/
static createFromPositionAndShift( position, shift ) {
const start = position;
const end = position.getShiftedBy( shift );
return shift > 0 ? new this( start, end ) : new this( end, start );
}
/**
* Creates a range from given parents and offsets.
*
* @param {module:engine/model/element~Element} startElement Start position parent element.
* @param {Number} startOffset Start position offset.
* @param {module:engine/model/element~Element} endElement End position parent element.
* @param {Number} endOffset End position offset.
* @returns {module:engine/model/range~Range}
*/
static createFromParentsAndOffsets( startElement, startOffset, endElement, endOffset ) {
return new this(
position_Position.createFromParentAndOffset( startElement, startOffset ),
position_Position.createFromParentAndOffset( endElement, endOffset )
);
}
/**
* Creates a new instance of `Range` which is equal to passed range.
*
* @param {module:engine/model/range~Range} range Range to clone.
* @returns {module:engine/model/range~Range}
*/
static createFromRange( range ) {
return new this( range.start, range.end );
}
/**
* Creates a range inside an {@link module:engine/model/element~Element element} which starts before the first child of
* that element and ends after the last child of that element.
*
* @param {module:engine/model/element~Element} element Element which is a parent for the range.
* @returns {module:engine/model/range~Range}
*/
static createIn( element ) {
return this.createFromParentsAndOffsets( element, 0, element, element.maxOffset );
}
/**
* Creates a range that starts before given {@link module:engine/model/item~Item model item} and ends after it.
*
* @param {module:engine/model/item~Item} item
* @returns {module:engine/model/range~Range}
*/
static createOn( item ) {
return this.createFromPositionAndShift( position_Position.createBefore( item ), item.offsetSize );
}
/**
* Creates a collapsed range at given {@link module:engine/model/position~Position position}
* or on the given {@link module:engine/model/item~Item item}.
*
* @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
* @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
* first parameter is a {@link module:engine/model/item~Item model item}.
*/
static createCollapsedAt( itemOrPosition, offset ) {
const start = position_Position.createAt( itemOrPosition, offset );
const end = position_Position.createFromPosition( start );
return new range_Range( start, end );
}
/**
* Combines all ranges from the passed array into a one range. At least one range has to be passed.
* Passed ranges must not have common parts.
*
* The first range from the array is a reference range. If other ranges start or end on the exactly same position where
* the reference range, they get combined into one range.
*
* [ ][] [ ][ ][ ][ ][] [ ] // Passed ranges, shown sorted
* [ ] // The result of the function if the first range was a reference range.
* [ ] // The result of the function if the third-to-seventh range was a reference range.
* [ ] // The result of the function if the last range was a reference range.
*
* @param {Array.<module:engine/model/range~Range>} ranges Ranges to combine.
* @returns {module:engine/model/range~Range} Combined range.
*/
static createFromRanges( ranges ) {
if ( ranges.length === 0 ) {
/**
* At least one range has to be passed to
* {@link module:engine/model/range~Range.createFromRanges `Range.createFromRanges()`}.
*
* @error range-create-from-ranges-empty-array
*/
throw new CKEditorError( 'range-create-from-ranges-empty-array: At least one range has to be passed.' );
} else if ( ranges.length == 1 ) {
return this.createFromRange( ranges[ 0 ] );
}
// 1. Set the first range in `ranges` array as a reference range.
// If we are going to return just a one range, one of the ranges need to be the reference one.
// Other ranges will be stuck to that range, if possible.
const ref = ranges[ 0 ];
// 2. Sort all the ranges so it's easier to process them.
ranges.sort( ( a, b ) => {
return a.start.isAfter( b.start ) ? 1 : -1;
} );
// 3. Check at which index the reference range is now.
const refIndex = ranges.indexOf( ref );
// 4. At this moment we don't need the original range.
// We are going to modify the result and we need to return a new instance of Range.
// We have to create a copy of the reference range.
const result = new this( ref.start, ref.end );
// 5. Ranges should be checked and glued starting from the range that is closest to the reference range.
// Since ranges are sorted, start with the range with index that is closest to reference range index.
for ( let i = refIndex - 1; i >= 0; i++ ) {
if ( ranges[ i ].end.isEqual( result.start ) ) {
result.start = position_Position.createFromPosition( ranges[ i ].start );
} else {
// If ranges are not starting/ending at the same position there is no point in looking further.
break;
}
}
// 6. Ranges should be checked and glued starting from the range that is closest to the reference range.
// Since ranges are sorted, start with the range with index that is closest to reference range index.
for ( let i = refIndex + 1; i < ranges.length; i++ ) {
if ( ranges[ i ].start.isEqual( result.end ) ) {
result.end = position_Position.createFromPosition( ranges[ i ].end );
} else {
// If ranges are not starting/ending at the same position there is no point in looking further.
break;
}
}
return result;
}
/**
* Creates a `Range` instance from given plain object (i.e. parsed JSON string).
*
* @param {Object} json Plain object to be converted to `Range`.
* @param {module:engine/model/document~Document} doc Document object that will be range owner.
* @returns {module:engine/model/element~Element} `Range` instance created using given plain object.
*/
static fromJSON( json, doc ) {
return new this( position_Position.fromJSON( json.start, doc ), position_Position.fromJSON( json.end, doc ) );
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_listCacheClear.js
/**
* Removes all key-value entries from the list cache.
*
* @private
* @name clear
* @memberOf ListCache
*/
function listCacheClear() {
this.__data__ = [];
}
/* harmony default export */ var _listCacheClear = (listCacheClear);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_assocIndexOf.js
/**
* Gets the index at which the `key` is found in `array` of key-value pairs.
*
* @private
* @param {Array} array The array to search.
* @param {*} key The key to search for.
* @returns {number} Returns the index of the matched value, else `-1`.
*/
function assocIndexOf(array, key) {
var length = array.length;
while (length--) {
if (lodash_eq(array[length][0], key)) {
return length;
}
}
return -1;
}
/* harmony default export */ var _assocIndexOf = (assocIndexOf);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_listCacheDelete.js
/** Used for built-in method references. */
var arrayProto = Array.prototype;
/** Built-in value references. */
var splice = arrayProto.splice;
/**
* Removes `key` and its value from the list cache.
*
* @private
* @name delete
* @memberOf ListCache
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function listCacheDelete(key) {
var data = this.__data__,
index = _assocIndexOf(data, key);
if (index < 0) {
return false;
}
var lastIndex = data.length - 1;
if (index == lastIndex) {
data.pop();
} else {
splice.call(data, index, 1);
}
return true;
}
/* harmony default export */ var _listCacheDelete = (listCacheDelete);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_listCacheGet.js
/**
* Gets the list cache value for `key`.
*
* @private
* @name get
* @memberOf ListCache
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function listCacheGet(key) {
var data = this.__data__,
index = _assocIndexOf(data, key);
return index < 0 ? undefined : data[index][1];
}
/* harmony default export */ var _listCacheGet = (listCacheGet);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_listCacheHas.js
/**
* Checks if a list cache value for `key` exists.
*
* @private
* @name has
* @memberOf ListCache
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function listCacheHas(key) {
return _assocIndexOf(this.__data__, key) > -1;
}
/* harmony default export */ var _listCacheHas = (listCacheHas);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_listCacheSet.js
/**
* Sets the list cache `key` to `value`.
*
* @private
* @name set
* @memberOf ListCache
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the list cache instance.
*/
function listCacheSet(key, value) {
var data = this.__data__,
index = _assocIndexOf(data, key);
if (index < 0) {
data.push([key, value]);
} else {
data[index][1] = value;
}
return this;
}
/* harmony default export */ var _listCacheSet = (listCacheSet);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_ListCache.js
/**
* Creates an list cache object.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function ListCache(entries) {
var index = -1,
length = entries ? entries.length : 0;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
// Add methods to `ListCache`.
ListCache.prototype.clear = _listCacheClear;
ListCache.prototype['delete'] = _listCacheDelete;
ListCache.prototype.get = _listCacheGet;
ListCache.prototype.has = _listCacheHas;
ListCache.prototype.set = _listCacheSet;
/* harmony default export */ var _ListCache = (ListCache);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_stackClear.js
/**
* Removes all key-value entries from the stack.
*
* @private
* @name clear
* @memberOf Stack
*/
function stackClear() {
this.__data__ = new _ListCache;
}
/* harmony default export */ var _stackClear = (stackClear);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_stackDelete.js
/**
* Removes `key` and its value from the stack.
*
* @private
* @name delete
* @memberOf Stack
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function stackDelete(key) {
return this.__data__['delete'](key);
}
/* harmony default export */ var _stackDelete = (stackDelete);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_stackGet.js
/**
* Gets the stack value for `key`.
*
* @private
* @name get
* @memberOf Stack
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function stackGet(key) {
return this.__data__.get(key);
}
/* harmony default export */ var _stackGet = (stackGet);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_stackHas.js
/**
* Checks if a stack value for `key` exists.
*
* @private
* @name has
* @memberOf Stack
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function stackHas(key) {
return this.__data__.has(key);
}
/* harmony default export */ var _stackHas = (stackHas);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_toSource.js
/** Used to resolve the decompiled source of functions. */
var _toSource_funcToString = Function.prototype.toString;
/**
* Converts `func` to its source code.
*
* @private
* @param {Function} func The function to process.
* @returns {string} Returns the source code.
*/
function toSource(func) {
if (func != null) {
try {
return _toSource_funcToString.call(func);
} catch (e) {}
try {
return (func + '');
} catch (e) {}
}
return '';
}
/* harmony default export */ var _toSource = (toSource);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isNative.js
/**
* Used to match `RegExp`
* [syntax characters](http://ecma-international.org/ecma-262/6.0/#sec-patterns).
*/
var reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
/** Used to detect host constructors (Safari). */
var reIsHostCtor = /^\[object .+?Constructor\]$/;
/** Used for built-in method references. */
var isNative_objectProto = Object.prototype;
/** Used to resolve the decompiled source of functions. */
var isNative_funcToString = Function.prototype.toString;
/** Used to check objects for own properties. */
var isNative_hasOwnProperty = isNative_objectProto.hasOwnProperty;
/** Used to detect if a method is native. */
var reIsNative = RegExp('^' +
isNative_funcToString.call(isNative_hasOwnProperty).replace(reRegExpChar, '\\$&')
.replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$'
);
/**
* Checks if `value` is a native function.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a native function,
* else `false`.
* @example
*
* _.isNative(Array.prototype.push);
* // => true
*
* _.isNative(_);
* // => false
*/
function isNative(value) {
if (!lodash_isObject(value)) {
return false;
}
var pattern = (lodash_isFunction(value) || _isHostObject(value)) ? reIsNative : reIsHostCtor;
return pattern.test(_toSource(value));
}
/* harmony default export */ var lodash_isNative = (isNative);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_getNative.js
/**
* Gets the native function at `key` of `object`.
*
* @private
* @param {Object} object The object to query.
* @param {string} key The key of the method to get.
* @returns {*} Returns the function if it's native, else `undefined`.
*/
function getNative(object, key) {
var value = object[key];
return lodash_isNative(value) ? value : undefined;
}
/* harmony default export */ var _getNative = (getNative);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_nativeCreate.js
/* Built-in method references that are verified to be native. */
var nativeCreate = _getNative(Object, 'create');
/* harmony default export */ var _nativeCreate = (nativeCreate);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_hashClear.js
/**
* Removes all key-value entries from the hash.
*
* @private
* @name clear
* @memberOf Hash
*/
function hashClear() {
this.__data__ = _nativeCreate ? _nativeCreate(null) : {};
}
/* harmony default export */ var _hashClear = (hashClear);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_hashDelete.js
/**
* Removes `key` and its value from the hash.
*
* @private
* @name delete
* @memberOf Hash
* @param {Object} hash The hash to modify.
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function hashDelete(key) {
return this.has(key) && delete this.__data__[key];
}
/* harmony default export */ var _hashDelete = (hashDelete);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_hashGet.js
/** Used to stand-in for `undefined` hash values. */
var HASH_UNDEFINED = '__lodash_hash_undefined__';
/** Used for built-in method references. */
var _hashGet_objectProto = Object.prototype;
/** Used to check objects for own properties. */
var _hashGet_hasOwnProperty = _hashGet_objectProto.hasOwnProperty;
/**
* Gets the hash value for `key`.
*
* @private
* @name get
* @memberOf Hash
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function hashGet(key) {
var data = this.__data__;
if (_nativeCreate) {
var result = data[key];
return result === HASH_UNDEFINED ? undefined : result;
}
return _hashGet_hasOwnProperty.call(data, key) ? data[key] : undefined;
}
/* harmony default export */ var _hashGet = (hashGet);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_hashHas.js
/** Used for built-in method references. */
var _hashHas_objectProto = Object.prototype;
/** Used to check objects for own properties. */
var _hashHas_hasOwnProperty = _hashHas_objectProto.hasOwnProperty;
/**
* Checks if a hash value for `key` exists.
*
* @private
* @name has
* @memberOf Hash
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function hashHas(key) {
var data = this.__data__;
return _nativeCreate ? data[key] !== undefined : _hashHas_hasOwnProperty.call(data, key);
}
/* harmony default export */ var _hashHas = (hashHas);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_hashSet.js
/** Used to stand-in for `undefined` hash values. */
var _hashSet_HASH_UNDEFINED = '__lodash_hash_undefined__';
/**
* Sets the hash `key` to `value`.
*
* @private
* @name set
* @memberOf Hash
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the hash instance.
*/
function hashSet(key, value) {
var data = this.__data__;
data[key] = (_nativeCreate && value === undefined) ? _hashSet_HASH_UNDEFINED : value;
return this;
}
/* harmony default export */ var _hashSet = (hashSet);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_Hash.js
/**
* Creates a hash object.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function Hash(entries) {
var index = -1,
length = entries ? entries.length : 0;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
// Add methods to `Hash`.
Hash.prototype.clear = _hashClear;
Hash.prototype['delete'] = _hashDelete;
Hash.prototype.get = _hashGet;
Hash.prototype.has = _hashHas;
Hash.prototype.set = _hashSet;
/* harmony default export */ var _Hash = (Hash);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_Map.js
/* Built-in method references that are verified to be native. */
var _Map_Map = _getNative(_root["a" /* default */], 'Map');
/* harmony default export */ var _Map = (_Map_Map);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_mapCacheClear.js
/**
* Removes all key-value entries from the map.
*
* @private
* @name clear
* @memberOf MapCache
*/
function mapCacheClear() {
this.__data__ = {
'hash': new _Hash,
'map': new (_Map || _ListCache),
'string': new _Hash
};
}
/* harmony default export */ var _mapCacheClear = (mapCacheClear);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_isKeyable.js
/**
* Checks if `value` is suitable for use as unique object key.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is suitable, else `false`.
*/
function isKeyable(value) {
var type = typeof value;
return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean')
? (value !== '__proto__')
: (value === null);
}
/* harmony default export */ var _isKeyable = (isKeyable);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_getMapData.js
/**
* Gets the data for `map`.
*
* @private
* @param {Object} map The map to query.
* @param {string} key The reference key.
* @returns {*} Returns the map data.
*/
function getMapData(map, key) {
var data = map.__data__;
return _isKeyable(key)
? data[typeof key == 'string' ? 'string' : 'hash']
: data.map;
}
/* harmony default export */ var _getMapData = (getMapData);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_mapCacheDelete.js
/**
* Removes `key` and its value from the map.
*
* @private
* @name delete
* @memberOf MapCache
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function mapCacheDelete(key) {
return _getMapData(this, key)['delete'](key);
}
/* harmony default export */ var _mapCacheDelete = (mapCacheDelete);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_mapCacheGet.js
/**
* Gets the map value for `key`.
*
* @private
* @name get
* @memberOf MapCache
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function mapCacheGet(key) {
return _getMapData(this, key).get(key);
}
/* harmony default export */ var _mapCacheGet = (mapCacheGet);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_mapCacheHas.js
/**
* Checks if a map value for `key` exists.
*
* @private
* @name has
* @memberOf MapCache
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function mapCacheHas(key) {
return _getMapData(this, key).has(key);
}
/* harmony default export */ var _mapCacheHas = (mapCacheHas);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_mapCacheSet.js
/**
* Sets the map `key` to `value`.
*
* @private
* @name set
* @memberOf MapCache
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the map cache instance.
*/
function mapCacheSet(key, value) {
_getMapData(this, key).set(key, value);
return this;
}
/* harmony default export */ var _mapCacheSet = (mapCacheSet);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_MapCache.js
/**
* Creates a map cache object to store key-value pairs.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function MapCache(entries) {
var index = -1,
length = entries ? entries.length : 0;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
// Add methods to `MapCache`.
MapCache.prototype.clear = _mapCacheClear;
MapCache.prototype['delete'] = _mapCacheDelete;
MapCache.prototype.get = _mapCacheGet;
MapCache.prototype.has = _mapCacheHas;
MapCache.prototype.set = _mapCacheSet;
/* harmony default export */ var _MapCache = (MapCache);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_stackSet.js
/** Used as the size to enable large array optimizations. */
var LARGE_ARRAY_SIZE = 200;
/**
* Sets the stack `key` to `value`.
*
* @private
* @name set
* @memberOf Stack
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the stack cache instance.
*/
function stackSet(key, value) {
var cache = this.__data__;
if (cache instanceof _ListCache && cache.__data__.length == LARGE_ARRAY_SIZE) {
cache = this.__data__ = new _MapCache(cache.__data__);
}
cache.set(key, value);
return this;
}
/* harmony default export */ var _stackSet = (stackSet);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_Stack.js
/**
* Creates a stack cache object to store key-value pairs.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function Stack(entries) {
this.__data__ = new _ListCache(entries);
}
// Add methods to `Stack`.
Stack.prototype.clear = _stackClear;
Stack.prototype['delete'] = _stackDelete;
Stack.prototype.get = _stackGet;
Stack.prototype.has = _stackHas;
Stack.prototype.set = _stackSet;
/* harmony default export */ var _Stack = (Stack);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_arrayEach.js
/**
* A specialized version of `_.forEach` for arrays without support for
* iteratee shorthands.
*
* @private
* @param {Array} array The array to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Array} Returns `array`.
*/
function arrayEach(array, iteratee) {
var index = -1,
length = array.length;
while (++index < length) {
if (iteratee(array[index], index, array) === false) {
break;
}
}
return array;
}
/* harmony default export */ var _arrayEach = (arrayEach);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseHas.js
/** Used for built-in method references. */
var _baseHas_objectProto = Object.prototype;
/** Used to check objects for own properties. */
var _baseHas_hasOwnProperty = _baseHas_objectProto.hasOwnProperty;
/**
* The base implementation of `_.has` without support for deep paths.
*
* @private
* @param {Object} object The object to query.
* @param {Array|string} key The key to check.
* @returns {boolean} Returns `true` if `key` exists, else `false`.
*/
function baseHas(object, key) {
// Avoid a bug in IE 10-11 where objects with a [[Prototype]] of `null`,
// that are composed entirely of index properties, return `false` for
// `hasOwnProperty` checks of them.
return _baseHas_hasOwnProperty.call(object, key) ||
(typeof object == 'object' && key in object && _getPrototype(object) === null);
}
/* harmony default export */ var _baseHas = (baseHas);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseKeys.js
/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeKeys = Object.keys;
/**
* The base implementation of `_.keys` which doesn't skip the constructor
* property of prototypes or treat sparse arrays as dense.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names.
*/
function baseKeys(object) {
return nativeKeys(Object(object));
}
/* harmony default export */ var _baseKeys = (baseKeys);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/keys.js
/**
* Creates an array of the own enumerable property names of `object`.
*
* **Note:** Non-object values are coerced to objects. See the
* [ES spec](http://ecma-international.org/ecma-262/6.0/#sec-object.keys)
* for more details.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Object
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names.
* @example
*
* function Foo() {
* this.a = 1;
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.keys(new Foo);
* // => ['a', 'b'] (iteration order is not guaranteed)
*
* _.keys('hi');
* // => ['0', '1']
*/
function keys_keys(object) {
var isProto = _isPrototype(object);
if (!(isProto || lodash_isArrayLike(object))) {
return _baseKeys(object);
}
var indexes = _indexKeys(object),
skipIndexes = !!indexes,
result = indexes || [],
length = result.length;
for (var key in object) {
if (_baseHas(object, key) &&
!(skipIndexes && (key == 'length' || _isIndex(key, length))) &&
!(isProto && key == 'constructor')) {
result.push(key);
}
}
return result;
}
/* harmony default export */ var lodash_keys = (keys_keys);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseAssign.js
/**
* The base implementation of `_.assign` without support for multiple sources
* or `customizer` functions.
*
* @private
* @param {Object} object The destination object.
* @param {Object} source The source object.
* @returns {Object} Returns `object`.
*/
function baseAssign(object, source) {
return object && _copyObject(source, lodash_keys(source), object);
}
/* harmony default export */ var _baseAssign = (baseAssign);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_cloneBuffer.js
/**
* Creates a clone of `buffer`.
*
* @private
* @param {Buffer} buffer The buffer to clone.
* @param {boolean} [isDeep] Specify a deep clone.
* @returns {Buffer} Returns the cloned buffer.
*/
function cloneBuffer(buffer, isDeep) {
if (isDeep) {
return buffer.slice();
}
var result = new buffer.constructor(buffer.length);
buffer.copy(result);
return result;
}
/* harmony default export */ var _cloneBuffer = (cloneBuffer);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_copyArray.js
/**
* Copies the values of `source` to `array`.
*
* @private
* @param {Array} source The array to copy values from.
* @param {Array} [array=[]] The array to copy values to.
* @returns {Array} Returns `array`.
*/
function copyArray(source, array) {
var index = -1,
length = source.length;
array || (array = Array(length));
while (++index < length) {
array[index] = source[index];
}
return array;
}
/* harmony default export */ var _copyArray = (copyArray);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_getSymbols.js
/** Built-in value references. */
var getOwnPropertySymbols = Object.getOwnPropertySymbols;
/**
* Creates an array of the own enumerable symbol properties of `object`.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of symbols.
*/
function getSymbols(object) {
// Coerce `object` to an object to avoid non-object errors in V8.
// See https://bugs.chromium.org/p/v8/issues/detail?id=3443 for more details.
return getOwnPropertySymbols(Object(object));
}
// Fallback for IE < 11.
if (!getOwnPropertySymbols) {
getSymbols = function() {
return [];
};
}
/* harmony default export */ var _getSymbols = (getSymbols);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_copySymbols.js
/**
* Copies own symbol properties of `source` to `object`.
*
* @private
* @param {Object} source The object to copy symbols from.
* @param {Object} [object={}] The object to copy symbols to.
* @returns {Object} Returns `object`.
*/
function copySymbols(source, object) {
return _copyObject(source, _getSymbols(source), object);
}
/* harmony default export */ var _copySymbols = (copySymbols);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_arrayPush.js
/**
* Appends the elements of `values` to `array`.
*
* @private
* @param {Array} array The array to modify.
* @param {Array} values The values to append.
* @returns {Array} Returns `array`.
*/
function arrayPush(array, values) {
var index = -1,
length = values.length,
offset = array.length;
while (++index < length) {
array[offset + index] = values[index];
}
return array;
}
/* harmony default export */ var _arrayPush = (arrayPush);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseGetAllKeys.js
/**
* The base implementation of `getAllKeys` and `getAllKeysIn` which uses
* `keysFunc` and `symbolsFunc` to get the enumerable property names and
* symbols of `object`.
*
* @private
* @param {Object} object The object to query.
* @param {Function} keysFunc The function to get the keys of `object`.
* @param {Function} symbolsFunc The function to get the symbols of `object`.
* @returns {Array} Returns the array of property names and symbols.
*/
function baseGetAllKeys(object, keysFunc, symbolsFunc) {
var result = keysFunc(object);
return lodash_isArray(object) ? result : _arrayPush(result, symbolsFunc(object));
}
/* harmony default export */ var _baseGetAllKeys = (baseGetAllKeys);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_getAllKeys.js
/**
* Creates an array of own enumerable property names and symbols of `object`.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names and symbols.
*/
function getAllKeys(object) {
return _baseGetAllKeys(object, lodash_keys, _getSymbols);
}
/* harmony default export */ var _getAllKeys = (getAllKeys);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_DataView.js
/* Built-in method references that are verified to be native. */
var DataView = _getNative(_root["a" /* default */], 'DataView');
/* harmony default export */ var _DataView = (DataView);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_Promise.js
/* Built-in method references that are verified to be native. */
var _Promise_Promise = _getNative(_root["a" /* default */], 'Promise');
/* harmony default export */ var _Promise = (_Promise_Promise);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_Set.js
/* Built-in method references that are verified to be native. */
var _Set_Set = _getNative(_root["a" /* default */], 'Set');
/* harmony default export */ var _Set = (_Set_Set);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_WeakMap.js
/* Built-in method references that are verified to be native. */
var _WeakMap_WeakMap = _getNative(_root["a" /* default */], 'WeakMap');
/* harmony default export */ var _WeakMap = (_WeakMap_WeakMap);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_getTag.js
/** `Object#toString` result references. */
var mapTag = '[object Map]',
_getTag_objectTag = '[object Object]',
promiseTag = '[object Promise]',
setTag = '[object Set]',
weakMapTag = '[object WeakMap]';
var dataViewTag = '[object DataView]';
/** Used for built-in method references. */
var _getTag_objectProto = Object.prototype;
/**
* Used to resolve the
* [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring)
* of values.
*/
var _getTag_objectToString = _getTag_objectProto.toString;
/** Used to detect maps, sets, and weakmaps. */
var dataViewCtorString = _toSource(_DataView),
mapCtorString = _toSource(_Map),
promiseCtorString = _toSource(_Promise),
setCtorString = _toSource(_Set),
weakMapCtorString = _toSource(_WeakMap);
/**
* Gets the `toStringTag` of `value`.
*
* @private
* @param {*} value The value to query.
* @returns {string} Returns the `toStringTag`.
*/
function getTag(value) {
return _getTag_objectToString.call(value);
}
// Fallback for data views, maps, sets, and weak maps in IE 11,
// for data views in Edge, and promises in Node.js.
if ((_DataView && getTag(new _DataView(new ArrayBuffer(1))) != dataViewTag) ||
(_Map && getTag(new _Map) != mapTag) ||
(_Promise && getTag(_Promise.resolve()) != promiseTag) ||
(_Set && getTag(new _Set) != setTag) ||
(_WeakMap && getTag(new _WeakMap) != weakMapTag)) {
getTag = function(value) {
var result = _getTag_objectToString.call(value),
Ctor = result == _getTag_objectTag ? value.constructor : undefined,
ctorString = Ctor ? _toSource(Ctor) : undefined;
if (ctorString) {
switch (ctorString) {
case dataViewCtorString: return dataViewTag;
case mapCtorString: return mapTag;
case promiseCtorString: return promiseTag;
case setCtorString: return setTag;
case weakMapCtorString: return weakMapTag;
}
}
return result;
};
}
/* harmony default export */ var _getTag = (getTag);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_initCloneArray.js
/** Used for built-in method references. */
var _initCloneArray_objectProto = Object.prototype;
/** Used to check objects for own properties. */
var _initCloneArray_hasOwnProperty = _initCloneArray_objectProto.hasOwnProperty;
/**
* Initializes an array clone.
*
* @private
* @param {Array} array The array to clone.
* @returns {Array} Returns the initialized clone.
*/
function initCloneArray(array) {
var length = array.length,
result = array.constructor(length);
// Add properties assigned by `RegExp#exec`.
if (length && typeof array[0] == 'string' && _initCloneArray_hasOwnProperty.call(array, 'index')) {
result.index = array.index;
result.input = array.input;
}
return result;
}
/* harmony default export */ var _initCloneArray = (initCloneArray);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_Uint8Array.js
/** Built-in value references. */
var _Uint8Array_Uint8Array = _root["a" /* default */].Uint8Array;
/* harmony default export */ var _Uint8Array = (_Uint8Array_Uint8Array);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_cloneArrayBuffer.js
/**
* Creates a clone of `arrayBuffer`.
*
* @private
* @param {ArrayBuffer} arrayBuffer The array buffer to clone.
* @returns {ArrayBuffer} Returns the cloned array buffer.
*/
function cloneArrayBuffer(arrayBuffer) {
var result = new arrayBuffer.constructor(arrayBuffer.byteLength);
new _Uint8Array(result).set(new _Uint8Array(arrayBuffer));
return result;
}
/* harmony default export */ var _cloneArrayBuffer = (cloneArrayBuffer);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_cloneDataView.js
/**
* Creates a clone of `dataView`.
*
* @private
* @param {Object} dataView The data view to clone.
* @param {boolean} [isDeep] Specify a deep clone.
* @returns {Object} Returns the cloned data view.
*/
function cloneDataView(dataView, isDeep) {
var buffer = isDeep ? _cloneArrayBuffer(dataView.buffer) : dataView.buffer;
return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength);
}
/* harmony default export */ var _cloneDataView = (cloneDataView);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_addMapEntry.js
/**
* Adds the key-value `pair` to `map`.
*
* @private
* @param {Object} map The map to modify.
* @param {Array} pair The key-value pair to add.
* @returns {Object} Returns `map`.
*/
function addMapEntry(map, pair) {
// Don't return `Map#set` because it doesn't return the map instance in IE 11.
map.set(pair[0], pair[1]);
return map;
}
/* harmony default export */ var _addMapEntry = (addMapEntry);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_arrayReduce.js
/**
* A specialized version of `_.reduce` for arrays without support for
* iteratee shorthands.
*
* @private
* @param {Array} array The array to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @param {*} [accumulator] The initial value.
* @param {boolean} [initAccum] Specify using the first element of `array` as
* the initial value.
* @returns {*} Returns the accumulated value.
*/
function arrayReduce(array, iteratee, accumulator, initAccum) {
var index = -1,
length = array.length;
if (initAccum && length) {
accumulator = array[++index];
}
while (++index < length) {
accumulator = iteratee(accumulator, array[index], index, array);
}
return accumulator;
}
/* harmony default export */ var _arrayReduce = (arrayReduce);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_mapToArray.js
/**
* Converts `map` to its key-value pairs.
*
* @private
* @param {Object} map The map to convert.
* @returns {Array} Returns the key-value pairs.
*/
function mapToArray(map) {
var index = -1,
result = Array(map.size);
map.forEach(function(value, key) {
result[++index] = [key, value];
});
return result;
}
/* harmony default export */ var _mapToArray = (mapToArray);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_cloneMap.js
/**
* Creates a clone of `map`.
*
* @private
* @param {Object} map The map to clone.
* @param {Function} cloneFunc The function to clone values.
* @param {boolean} [isDeep] Specify a deep clone.
* @returns {Object} Returns the cloned map.
*/
function cloneMap(map, isDeep, cloneFunc) {
var array = isDeep ? cloneFunc(_mapToArray(map), true) : _mapToArray(map);
return _arrayReduce(array, _addMapEntry, new map.constructor);
}
/* harmony default export */ var _cloneMap = (cloneMap);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_cloneRegExp.js
/** Used to match `RegExp` flags from their coerced string values. */
var reFlags = /\w*$/;
/**
* Creates a clone of `regexp`.
*
* @private
* @param {Object} regexp The regexp to clone.
* @returns {Object} Returns the cloned regexp.
*/
function cloneRegExp(regexp) {
var result = new regexp.constructor(regexp.source, reFlags.exec(regexp));
result.lastIndex = regexp.lastIndex;
return result;
}
/* harmony default export */ var _cloneRegExp = (cloneRegExp);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_addSetEntry.js
/**
* Adds `value` to `set`.
*
* @private
* @param {Object} set The set to modify.
* @param {*} value The value to add.
* @returns {Object} Returns `set`.
*/
function addSetEntry(set, value) {
set.add(value);
return set;
}
/* harmony default export */ var _addSetEntry = (addSetEntry);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_setToArray.js
/**
* Converts `set` to an array of its values.
*
* @private
* @param {Object} set The set to convert.
* @returns {Array} Returns the values.
*/
function setToArray(set) {
var index = -1,
result = Array(set.size);
set.forEach(function(value) {
result[++index] = value;
});
return result;
}
/* harmony default export */ var _setToArray = (setToArray);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_cloneSet.js
/**
* Creates a clone of `set`.
*
* @private
* @param {Object} set The set to clone.
* @param {Function} cloneFunc The function to clone values.
* @param {boolean} [isDeep] Specify a deep clone.
* @returns {Object} Returns the cloned set.
*/
function cloneSet(set, isDeep, cloneFunc) {
var array = isDeep ? cloneFunc(_setToArray(set), true) : _setToArray(set);
return _arrayReduce(array, _addSetEntry, new set.constructor);
}
/* harmony default export */ var _cloneSet = (cloneSet);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_Symbol.js
/** Built-in value references. */
var _Symbol_Symbol = _root["a" /* default */].Symbol;
/* harmony default export */ var _Symbol = (_Symbol_Symbol);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_cloneSymbol.js
/** Used to convert symbols to primitives and strings. */
var symbolProto = _Symbol ? _Symbol.prototype : undefined,
symbolValueOf = symbolProto ? symbolProto.valueOf : undefined;
/**
* Creates a clone of the `symbol` object.
*
* @private
* @param {Object} symbol The symbol object to clone.
* @returns {Object} Returns the cloned symbol object.
*/
function cloneSymbol(symbol) {
return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {};
}
/* harmony default export */ var _cloneSymbol = (cloneSymbol);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_cloneTypedArray.js
/**
* Creates a clone of `typedArray`.
*
* @private
* @param {Object} typedArray The typed array to clone.
* @param {boolean} [isDeep] Specify a deep clone.
* @returns {Object} Returns the cloned typed array.
*/
function cloneTypedArray(typedArray, isDeep) {
var buffer = isDeep ? _cloneArrayBuffer(typedArray.buffer) : typedArray.buffer;
return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length);
}
/* harmony default export */ var _cloneTypedArray = (cloneTypedArray);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_initCloneByTag.js
/** `Object#toString` result references. */
var boolTag = '[object Boolean]',
dateTag = '[object Date]',
_initCloneByTag_mapTag = '[object Map]',
numberTag = '[object Number]',
regexpTag = '[object RegExp]',
_initCloneByTag_setTag = '[object Set]',
_initCloneByTag_stringTag = '[object String]',
_initCloneByTag_symbolTag = '[object Symbol]';
var arrayBufferTag = '[object ArrayBuffer]',
_initCloneByTag_dataViewTag = '[object DataView]',
float32Tag = '[object Float32Array]',
float64Tag = '[object Float64Array]',
int8Tag = '[object Int8Array]',
int16Tag = '[object Int16Array]',
int32Tag = '[object Int32Array]',
uint8Tag = '[object Uint8Array]',
uint8ClampedTag = '[object Uint8ClampedArray]',
uint16Tag = '[object Uint16Array]',
uint32Tag = '[object Uint32Array]';
/**
* Initializes an object clone based on its `toStringTag`.
*
* **Note:** This function only supports cloning values with tags of
* `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`.
*
* @private
* @param {Object} object The object to clone.
* @param {string} tag The `toStringTag` of the object to clone.
* @param {Function} cloneFunc The function to clone values.
* @param {boolean} [isDeep] Specify a deep clone.
* @returns {Object} Returns the initialized clone.
*/
function initCloneByTag(object, tag, cloneFunc, isDeep) {
var Ctor = object.constructor;
switch (tag) {
case arrayBufferTag:
return _cloneArrayBuffer(object);
case boolTag:
case dateTag:
return new Ctor(+object);
case _initCloneByTag_dataViewTag:
return _cloneDataView(object, isDeep);
case float32Tag: case float64Tag:
case int8Tag: case int16Tag: case int32Tag:
case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag:
return _cloneTypedArray(object, isDeep);
case _initCloneByTag_mapTag:
return _cloneMap(object, isDeep, cloneFunc);
case numberTag:
case _initCloneByTag_stringTag:
return new Ctor(object);
case regexpTag:
return _cloneRegExp(object);
case _initCloneByTag_setTag:
return _cloneSet(object, isDeep, cloneFunc);
case _initCloneByTag_symbolTag:
return _cloneSymbol(object);
}
}
/* harmony default export */ var _initCloneByTag = (initCloneByTag);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseCreate.js
/** Built-in value references. */
var objectCreate = Object.create;
/**
* The base implementation of `_.create` without support for assigning
* properties to the created object.
*
* @private
* @param {Object} prototype The object to inherit from.
* @returns {Object} Returns the new object.
*/
function baseCreate(proto) {
return lodash_isObject(proto) ? objectCreate(proto) : {};
}
/* harmony default export */ var _baseCreate = (baseCreate);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_initCloneObject.js
/**
* Initializes an object clone.
*
* @private
* @param {Object} object The object to clone.
* @returns {Object} Returns the initialized clone.
*/
function initCloneObject(object) {
return (typeof object.constructor == 'function' && !_isPrototype(object))
? _baseCreate(_getPrototype(object))
: {};
}
/* harmony default export */ var _initCloneObject = (initCloneObject);
// EXTERNAL MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isBuffer.js
var isBuffer = __webpack_require__(7);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseClone.js
/** `Object#toString` result references. */
var _baseClone_argsTag = '[object Arguments]',
arrayTag = '[object Array]',
_baseClone_boolTag = '[object Boolean]',
_baseClone_dateTag = '[object Date]',
errorTag = '[object Error]',
_baseClone_funcTag = '[object Function]',
_baseClone_genTag = '[object GeneratorFunction]',
_baseClone_mapTag = '[object Map]',
_baseClone_numberTag = '[object Number]',
_baseClone_objectTag = '[object Object]',
_baseClone_regexpTag = '[object RegExp]',
_baseClone_setTag = '[object Set]',
_baseClone_stringTag = '[object String]',
_baseClone_symbolTag = '[object Symbol]',
_baseClone_weakMapTag = '[object WeakMap]';
var _baseClone_arrayBufferTag = '[object ArrayBuffer]',
_baseClone_dataViewTag = '[object DataView]',
_baseClone_float32Tag = '[object Float32Array]',
_baseClone_float64Tag = '[object Float64Array]',
_baseClone_int8Tag = '[object Int8Array]',
_baseClone_int16Tag = '[object Int16Array]',
_baseClone_int32Tag = '[object Int32Array]',
_baseClone_uint8Tag = '[object Uint8Array]',
_baseClone_uint8ClampedTag = '[object Uint8ClampedArray]',
_baseClone_uint16Tag = '[object Uint16Array]',
_baseClone_uint32Tag = '[object Uint32Array]';
/** Used to identify `toStringTag` values supported by `_.clone`. */
var cloneableTags = {};
cloneableTags[_baseClone_argsTag] = cloneableTags[arrayTag] =
cloneableTags[_baseClone_arrayBufferTag] = cloneableTags[_baseClone_dataViewTag] =
cloneableTags[_baseClone_boolTag] = cloneableTags[_baseClone_dateTag] =
cloneableTags[_baseClone_float32Tag] = cloneableTags[_baseClone_float64Tag] =
cloneableTags[_baseClone_int8Tag] = cloneableTags[_baseClone_int16Tag] =
cloneableTags[_baseClone_int32Tag] = cloneableTags[_baseClone_mapTag] =
cloneableTags[_baseClone_numberTag] = cloneableTags[_baseClone_objectTag] =
cloneableTags[_baseClone_regexpTag] = cloneableTags[_baseClone_setTag] =
cloneableTags[_baseClone_stringTag] = cloneableTags[_baseClone_symbolTag] =
cloneableTags[_baseClone_uint8Tag] = cloneableTags[_baseClone_uint8ClampedTag] =
cloneableTags[_baseClone_uint16Tag] = cloneableTags[_baseClone_uint32Tag] = true;
cloneableTags[errorTag] = cloneableTags[_baseClone_funcTag] =
cloneableTags[_baseClone_weakMapTag] = false;
/**
* The base implementation of `_.clone` and `_.cloneDeep` which tracks
* traversed objects.
*
* @private
* @param {*} value The value to clone.
* @param {boolean} [isDeep] Specify a deep clone.
* @param {boolean} [isFull] Specify a clone including symbols.
* @param {Function} [customizer] The function to customize cloning.
* @param {string} [key] The key of `value`.
* @param {Object} [object] The parent object of `value`.
* @param {Object} [stack] Tracks traversed objects and their clone counterparts.
* @returns {*} Returns the cloned value.
*/
function baseClone(value, isDeep, isFull, customizer, key, object, stack) {
var result;
if (customizer) {
result = object ? customizer(value, key, object, stack) : customizer(value);
}
if (result !== undefined) {
return result;
}
if (!lodash_isObject(value)) {
return value;
}
var isArr = lodash_isArray(value);
if (isArr) {
result = _initCloneArray(value);
if (!isDeep) {
return _copyArray(value, result);
}
} else {
var tag = _getTag(value),
isFunc = tag == _baseClone_funcTag || tag == _baseClone_genTag;
if (Object(isBuffer["a" /* default */])(value)) {
return _cloneBuffer(value, isDeep);
}
if (tag == _baseClone_objectTag || tag == _baseClone_argsTag || (isFunc && !object)) {
if (_isHostObject(value)) {
return object ? value : {};
}
result = _initCloneObject(isFunc ? {} : value);
if (!isDeep) {
return _copySymbols(value, _baseAssign(result, value));
}
} else {
if (!cloneableTags[tag]) {
return object ? value : {};
}
result = _initCloneByTag(value, tag, baseClone, isDeep);
}
}
// Check for circular references and return its corresponding clone.
stack || (stack = new _Stack);
var stacked = stack.get(value);
if (stacked) {
return stacked;
}
stack.set(value, result);
if (!isArr) {
var props = isFull ? _getAllKeys(value) : lodash_keys(value);
}
// Recursively populate clone (susceptible to call stack limits).
_arrayEach(props || value, function(subValue, key) {
if (props) {
key = subValue;
subValue = value[key];
}
_assignValue(result, key, baseClone(subValue, isDeep, isFull, customizer, key, value, stack));
});
return result;
}
/* harmony default export */ var _baseClone = (baseClone);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/clone.js
/**
* Creates a shallow clone of `value`.
*
* **Note:** This method is loosely based on the
* [structured clone algorithm](https://mdn.io/Structured_clone_algorithm)
* and supports cloning arrays, array buffers, booleans, date objects, maps,
* numbers, `Object` objects, regexes, sets, strings, symbols, and typed
* arrays. The own enumerable properties of `arguments` objects are cloned
* as plain objects. An empty object is returned for uncloneable values such
* as error objects, functions, DOM nodes, and WeakMaps.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to clone.
* @returns {*} Returns the cloned value.
* @see _.cloneDeep
* @example
*
* var objects = [{ 'a': 1 }, { 'b': 2 }];
*
* var shallow = _.clone(objects);
* console.log(shallow[0] === objects[0]);
* // => true
*/
function clone_clone(value) {
return _baseClone(value, false, true);
}
/* harmony default export */ var lodash_clone = (clone_clone);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/node.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/view/node
*/
/**
* Abstract tree view node class.
*
* @abstract
*/
class view_node_Node {
/**
* Creates a tree view node.
*
* This is an abstract class, so this constructor should not be used directly.
*/
constructor() {
/**
* Parent element. Null by default. Set by {@link module:engine/view/element~Element#insertChildren}.
*
* @readonly
* @member {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment|null}
*/
this.parent = null;
}
/**
* Index of the node in the parent element or null if the node has no parent.
*
* Accessing this property throws an error if this node's parent element does not contain it.
* This means that view tree got broken.
*
* @readonly
* @type {Number|null}
*/
get index() {
let pos;
if ( !this.parent ) {
return null;
}
// No parent or child doesn't exist in parent's children.
if ( ( pos = this.parent.getChildIndex( this ) ) == -1 ) {
/**
* The node's parent does not contain this node. It means that the document tree is corrupted.
*
* @error view-node-not-found-in-parent
*/
throw new CKEditorError( 'view-node-not-found-in-parent: The node\'s parent does not contain this node.' );
}
return pos;
}
/**
* Node's next sibling, or `null` if it is the last child.
*
* @readonly
* @type {module:engine/view/node~Node|null}
*/
get nextSibling() {
const index = this.index;
return ( index !== null && this.parent.getChild( index + 1 ) ) || null;
}
/**
* Node's previous sibling, or `null` if it is the first child.
*
* @readonly
* @type {module:engine/view/node~Node|null}
*/
get previousSibling() {
const index = this.index;
return ( index !== null && this.parent.getChild( index - 1 ) ) || null;
}
/**
* Top-most ancestor of the node. If the node has no parent it is the root itself.
*
* @readonly
* @type {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment}
*/
get root() {
let root = this; // eslint-disable-line consistent-this
while ( root.parent ) {
root = root.parent;
}
return root;
}
/**
* {@link module:engine/view/document~Document View document} that owns this node, or `null` if the node is inside
* {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
*
* @readonly
* @type {module:engine/view/document~Document|null}
*/
get document() {
// Parent might be Node, null or DocumentFragment.
if ( this.parent instanceof view_node_Node ) {
return this.parent.document;
} else {
return null;
}
}
/**
* Returns ancestors array of this node.
*
* @param {Object} options Options object.
* @param {Boolean} [options.includeSelf=false] When set to `true` this node will be also included in parent's array.
* @param {Boolean} [options.parentFirst=false] When set to `true`, array will be sorted from node's parent to root element,
* otherwise root element will be the first item in the array.
* @returns {Array} Array with ancestors.
*/
getAncestors( options = { includeSelf: false, parentFirst: false } ) {
const ancestors = [];
let parent = options.includeSelf ? this : this.parent;
while ( parent ) {
ancestors[ options.parentFirst ? 'push' : 'unshift' ]( parent );
parent = parent.parent;
}
return ancestors;
}
/**
* Returns a {@link module:engine/view/element~Element} or {@link module:engine/view/documentfragment~DocumentFragment}
* which is a common ancestor of both nodes.
*
* @param {module:engine/view/node~Node} node The second node.
* @param {Object} options Options object.
* @param {Boolean} [options.includeSelf=false] When set to `true` both nodes will be considered "ancestors" too.
* Which means that if e.g. node A is inside B, then their common ancestor will be B.
* @returns {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment|null}
*/
getCommonAncestor( node, options = {} ) {
const ancestorsA = this.getAncestors( options );
const ancestorsB = node.getAncestors( options );
let i = 0;
while ( ancestorsA[ i ] == ancestorsB[ i ] && ancestorsA[ i ] ) {
i++;
}
return i === 0 ? null : ancestorsA[ i - 1 ];
}
/**
* Removes node from parent.
*/
remove() {
this.parent.removeChildren( this.index );
}
/**
* @param {module:engine/view/document~ChangeType} type Type of the change.
* @param {module:engine/view/node~Node} node Changed node.
* @fires change
*/
_fireChange( type, node ) {
this.fire( 'change:' + type, node );
if ( this.parent ) {
this.parent._fireChange( type, node );
}
}
/**
* Custom toJSON method to solve child-parent circular dependencies.
*
* @returns {Object} Clone of this object with the parent property removed.
*/
toJSON() {
const json = lodash_clone( this );
// Due to circular references we need to remove parent reference.
delete json.parent;
return json;
}
/**
* Clones this node.
*
* @method #clone
* @returns {module:engine/view/node~Node} Clone of this node.
*/
/**
* Checks if provided node is similar to this node.
*
* @method #isSimilar
* @returns {Boolean} True if nodes are similar.
*/
/**
* Checks whether given view tree object is of given type.
*
* This method is useful when processing view tree objects that are of unknown type. For example, a function
* may return {@link module:engine/view/documentfragment~DocumentFragment} or {@link module:engine/view/node~Node}
* that can be either text node or element. This method can be used to check what kind of object is returned.
*
* obj.is( 'node' ); // true for any node, false for document fragment
* obj.is( 'documentFragment' ); // true for document fragment, false for any node
* obj.is( 'element' ); // true for any element, false for text node or document fragment
* obj.is( 'element', 'p' ); // true only for element which name is 'p'
* obj.is( 'p' ); // shortcut for obj.is( 'element', 'p' )
* obj.is( 'text' ); // true for text node, false for element and document fragment
*
* @method #is
* @param {'element'|'containerElement'|'attributeElement'|'emptyElement'|'uiElement'|
* 'rootElement'|'documentFragment'|'text'|'textProxy'} type
* @returns {Boolean}
*/
}
/**
* Fired when list of {@link module:engine/view/element~Element elements} children changes.
*
* Change event is bubbled – it is fired on all ancestors.
*
* @event change:children
* @param {module:engine/view/node~Node} changedNode
*/
/**
* Fired when list of {@link module:engine/view/element~Element elements} attributes changes.
*
* Change event is bubbled – it is fired on all ancestors.
*
* @event change:attributes
* @param {module:engine/view/node~Node} changedNode
*/
/**
* Fired when {@link module:engine/view/text~Text text nodes} data changes.
*
* Change event is bubbled – it is fired on all ancestors.
*
* @event change:text
* @param {module:engine/view/node~Node} changedNode
*/
/**
* @event change
*/
mix( view_node_Node, emittermixin );
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/text.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/view/text
*/
/**
* Tree view text node.
*
* @extends module:engine/view/node~Node
*/
class view_text_Text extends view_node_Node {
/**
* Creates a tree view text node.
*
* @param {String} data Text.
*/
constructor( data ) {
super();
/**
* The text content.
*
* Setting the data fires the {@link module:engine/view/node~Node#event:change:text change event}.
*
* @private
* @member {String} module:engine/view/text~Text#_data
*/
this._data = data;
}
/**
* Clones this node.
*
* @returns {module:engine/view/text~Text} Text node that is a clone of this node.
*/
clone() {
return new view_text_Text( this.data );
}
/**
* @inheritDoc
*/
is( type ) {
return type == 'text';
}
/**
* The text content.
*
* Setting the data fires the {@link module:engine/view/node~Node#event:change:text change event}.
*/
get data() {
return this._data;
}
set data( data ) {
this._fireChange( 'text', this );
this._data = data;
}
/**
* Checks if this text node is similar to other text node.
* Both nodes should have the same data to be considered as similar.
*
* @param {module:engine/view/text~Text} otherNode Node to check if it is same as this node.
* @returns {Boolean}
*/
isSimilar( otherNode ) {
if ( !( otherNode instanceof view_text_Text ) ) {
return false;
}
return this === otherNode || this.data === otherNode.data;
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/matcher.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/view/matcher
*/
/**
* View matcher class.
* Instance of this class can be used to find {@link module:engine/view/element~Element elements} that match given pattern.
*/
class Matcher {
/**
* Creates new instance of Matcher.
*
* @param {String|RegExp|Object} [pattern] Match patterns. See {@link module:engine/view/matcher~Matcher#add add method} for
* more information.
*/
constructor( ...pattern ) {
this._patterns = [];
this.add( ...pattern );
}
/**
* Adds pattern or patterns to matcher instance.
*
* Example patterns matching element's name:
*
* // String.
* matcher.add( 'div' );
* matcher.add( { name: 'div' } );
*
* // Regular expression.
* matcher.add( /^\w/ );
* matcher.add( { name: /^\w/ } );
*
* Example pattern matching element's attributes:
*
* matcher.add( {
* attribute: {
* title: 'foobar',
* foo: /^\w+/
* }
* } );
*
* Example patterns matching element's classes:
*
* // Single class.
* matcher.add( {
* class: 'foobar'
* } );
*
* // Single class using regular expression.
* matcher.add( {
* class: /foo.../
* } );
*
* // Multiple classes to match.
* matcher.add( {
* class: [ 'baz', 'bar', /foo.../ ]
* } ):
*
* Example pattern matching element's styles:
*
* matcher.add( {
* style: {
* position: 'absolute',
* color: /^\w*blue$/
* }
* } );
*
* Example function pattern:
*
* matcher.add( ( element ) => {
* // Result of this function will be included in `match`
* // property of the object returned from matcher.match() call.
* if ( element.name === 'div' && element.childCount > 0 ) {
* return { name: true };
* }
*
* return null;
* } );
*
* Multiple patterns can be added in one call:
*
* matcher.add( 'div', { class: 'foobar' } );
*
* @param {Object|String|RegExp|Function} pattern Object describing pattern details. If string or regular expression
* is provided it will be used to match element's name. Pattern can be also provided in a form
* of a function - then this function will be called with each {@link module:engine/view/element~Element element} as a parameter.
* Function's return value will be stored under `match` key of the object returned from
* {@link module:engine/view/matcher~Matcher#match match} or {@link module:engine/view/matcher~Matcher#matchAll matchAll} methods.
* @param {String|RegExp} [pattern.name] Name or regular expression to match element's name.
* @param {Object} [pattern.attribute] Object with key-value pairs representing attributes to match. Each object key
* represents attribute name. Value under that key can be either a string or a regular expression and it will be
* used to match attribute value.
* @param {String|RegExp|Array} [pattern.class] Class name or array of class names to match. Each name can be
* provided in a form of string or regular expression.
* @param {Object} [pattern.style] Object with key-value pairs representing styles to match. Each object key
* represents style name. Value under that key can be either a string or a regular expression and it will be used
* to match style value.
*/
add( ...pattern ) {
for ( let item of pattern ) {
// String or RegExp pattern is used as element's name.
if ( typeof item == 'string' || item instanceof RegExp ) {
item = { name: item };
}
// Single class name/RegExp can be provided.
if ( item.class && ( typeof item.class == 'string' || item.class instanceof RegExp ) ) {
item.class = [ item.class ];
}
this._patterns.push( item );
}
}
/**
* Matches elements for currently stored patterns. Returns match information about first found
* {@link module:engine/view/element~Element element}, otherwise returns `null`.
*
* Example of returned object:
*
* {
* element: <instance of found element>,
* pattern: <pattern used to match found element>,
* match: {
* name: true,
* attribute: [ 'title', 'href' ],
* class: [ 'foo' ],
* style: [ 'color', 'position' ]
* }
* }
*
* @see module:engine/view/matcher~Matcher#add
* @see module:engine/view/matcher~Matcher#matchAll
* @param {...module:engine/view/element~Element} element View element to match against stored patterns.
* @returns {Object|null} result
* @returns {module:engine/view/element~Element} result.element Matched view element.
* @returns {Object|String|RegExp|Function} result.pattern Pattern that was used to find matched element.
* @returns {Object} result.match Object representing matched element parts.
* @returns {Boolean} [result.match.name] True if name of the element was matched.
* @returns {Array} [result.match.attribute] Array with matched attribute names.
* @returns {Array} [result.match.class] Array with matched class names.
* @returns {Array} [result.match.style] Array with matched style names.
*/
match( ...element ) {
for ( const singleElement of element ) {
for ( const pattern of this._patterns ) {
const match = isElementMatching( singleElement, pattern );
if ( match ) {
return {
element: singleElement,
pattern,
match
};
}
}
}
return null;
}
/**
* Matches elements for currently stored patterns. Returns array of match information with all found
* {@link module:engine/view/element~Element elements}. If no element is found - returns `null`.
*
* @see module:engine/view/matcher~Matcher#add
* @see module:engine/view/matcher~Matcher#match
* @param {...module:engine/view/element~Element} element View element to match against stored patterns.
* @returns {Array.<Object>|null} Array with match information about found elements or `null`. For more information
* see {@link module:engine/view/matcher~Matcher#match match method} description.
*/
matchAll( ...element ) {
const results = [];
for ( const singleElement of element ) {
for ( const pattern of this._patterns ) {
const match = isElementMatching( singleElement, pattern );
if ( match ) {
results.push( {
element: singleElement,
pattern,
match
} );
}
}
}
return results.length > 0 ? results : null;
}
/**
* Returns the name of the element to match if there is exactly one pattern added to the matcher instance
* and it matches element name defined by `string` (not `RegExp`). Otherwise, returns `null`.
*
* @returns {String|null} Element name trying to match.
*/
getElementName() {
if ( this._patterns.length !== 1 ) {
return null;
}
const pattern = this._patterns[ 0 ];
const name = pattern.name;
return ( typeof pattern != 'function' && name && !( name instanceof RegExp ) ) ? name : null;
}
}
// Returns match information if {@link module:engine/view/element~Element element} is matching provided pattern.
// If element cannot be matched to provided pattern - returns `null`.
//
// @param {module:engine/view/element~Element} element
// @param {Object|String|RegExp|Function} pattern
// @returns {Object|null} Returns object with match information or null if element is not matching.
function isElementMatching( element, pattern ) {
// If pattern is provided as function - return result of that function;
if ( typeof pattern == 'function' ) {
return pattern( element );
}
const match = {};
// Check element's name.
if ( pattern.name ) {
match.name = matchName( pattern.name, element.name );
if ( !match.name ) {
return null;
}
}
// Check element's attributes.
if ( pattern.attribute ) {
match.attribute = matchAttributes( pattern.attribute, element );
if ( !match.attribute ) {
return null;
}
}
// Check element's classes.
if ( pattern.class ) {
match.class = matchClasses( pattern.class, element );
if ( !match.class ) {
return false;
}
}
// Check element's styles.
if ( pattern.style ) {
match.style = matchStyles( pattern.style, element );
if ( !match.style ) {
return false;
}
}
return match;
}
// Checks if name can be matched by provided pattern.
//
// @param {String|RegExp} pattern
// @param {String} name
// @returns {Boolean} Returns `true` if name can be matched, `false` otherwise.
function matchName( pattern, name ) {
// If pattern is provided as RegExp - test against this regexp.
if ( pattern instanceof RegExp ) {
return pattern.test( name );
}
return pattern === name;
}
// Checks if attributes of provided element can be matched against provided patterns.
//
// @param {Object} patterns Object with information about attributes to match. Each key of the object will be
// used as attribute name. Value of each key can be a string or regular expression to match against attribute value.
// @param {module:engine/view/element~Element} element Element which attributes will be tested.
// @returns {Array|null} Returns array with matched attribute names or `null` if no attributes were matched.
function matchAttributes( patterns, element ) {
const match = [];
for ( const name in patterns ) {
const pattern = patterns[ name ];
if ( element.hasAttribute( name ) ) {
const attribute = element.getAttribute( name );
if ( pattern instanceof RegExp ) {
if ( pattern.test( attribute ) ) {
match.push( name );
} else {
return null;
}
} else if ( attribute === pattern ) {
match.push( name );
} else {
return null;
}
} else {
return null;
}
}
return match;
}
// Checks if classes of provided element can be matched against provided patterns.
//
// @param {Array.<String|RegExp>} patterns Array of strings or regular expressions to match against element's classes.
// @param {module:engine/view/element~Element} element Element which classes will be tested.
// @returns {Array|null} Returns array with matched class names or `null` if no classes were matched.
function matchClasses( patterns, element ) {
const match = [];
for ( const pattern of patterns ) {
if ( pattern instanceof RegExp ) {
const classes = element.getClassNames();
for ( const name of classes ) {
if ( pattern.test( name ) ) {
match.push( name );
}
}
if ( match.length === 0 ) {
return null;
}
} else if ( element.hasClass( pattern ) ) {
match.push( pattern );
} else {
return null;
}
}
return match;
}
// Checks if styles of provided element can be matched against provided patterns.
//
// @param {Object} patterns Object with information about styles to match. Each key of the object will be
// used as style name. Value of each key can be a string or regular expression to match against style value.
// @param {module:engine/view/element~Element} element Element which styles will be tested.
// @returns {Array|null} Returns array with matched style names or `null` if no styles were matched.
function matchStyles( patterns, element ) {
const match = [];
for ( const name in patterns ) {
const pattern = patterns[ name ];
if ( element.hasStyle( name ) ) {
const style = element.getStyle( name );
if ( pattern instanceof RegExp ) {
if ( pattern.test( style ) ) {
match.push( name );
} else {
return null;
}
} else if ( style === pattern ) {
match.push( name );
} else {
return null;
}
} else {
return null;
}
}
return match;
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/element.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/view/element
*/
/**
* View element.
*
* Editing engine does not define fixed HTML DTD. This is why the type of the {@link module:engine/view/element~Element} need to
* be defined by the feature developer. Creating an element you should use {@link module:engine/view/containerelement~ContainerElement}
* class, {@link module:engine/view/attributeelement~AttributeElement} class or {@link module:engine/view/emptyelement~EmptyElement} class.
*
* Note that for view elements which are not created from model, like elements from mutations, paste or
* {@link module:engine/controller/datacontroller~DataController#set data.set} it is not possible to define the type of the element, so
* these will be instances of the {@link module:engine/view/element~Element}.
*
* @extends module:engine/view/node~Node
*/
class view_element_Element extends view_node_Node {
/**
* Creates a view element.
*
* Attributes can be passed in various formats:
*
* new Element( 'div', { 'class': 'editor', 'contentEditable': 'true' } ); // object
* new Element( 'div', [ [ 'class', 'editor' ], [ 'contentEditable', 'true' ] ] ); // map-like iterator
* new Element( 'div', mapOfAttributes ); // map
*
* @param {String} name Node name.
* @param {Object|Iterable} [attrs] Collection of attributes.
* @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
* List of nodes to be inserted into created element.
*/
constructor( name, attrs, children ) {
super();
/**
* Name of the element.
*
* @readonly
* @member {String}
*/
this.name = name;
/**
* Map of attributes, where attributes names are keys and attributes values are values.
*
* @protected
* @member {Map} #_attrs
*/
if ( lodash_isPlainObject( attrs ) ) {
this._attrs = objectToMap( attrs );
} else {
this._attrs = new Map( attrs );
}
/**
* Array of child nodes.
*
* @protected
* @member {Array.<module:engine/view/node~Node>}
*/
this._children = [];
if ( children ) {
this.insertChildren( 0, children );
}
/**
* Set of classes associated with element instance.
*
* @protected
* @member {Set}
*/
this._classes = new Set();
if ( this._attrs.has( 'class' ) ) {
// Remove class attribute and handle it by class set.
const classString = this._attrs.get( 'class' );
parseClasses( this._classes, classString );
this._attrs.delete( 'class' );
}
/**
* Map of styles.
*
* @protected
* @member {Set} module:engine/view/element~Element#_styles
*/
this._styles = new Map();
if ( this._attrs.has( 'style' ) ) {
// Remove style attribute and handle it by styles map.
parseInlineStyles( this._styles, this._attrs.get( 'style' ) );
this._attrs.delete( 'style' );
}
/**
* Map of custom properties.
* Custom properties can be added to element instance, will be cloned but not rendered into DOM.
*
* @protected
* @memeber {Map}
*/
this._customProperties = new Map();
}
/**
* Number of element's children.
*
* @readonly
* @type {Number}
*/
get childCount() {
return this._children.length;
}
/**
* Is `true` if there are no nodes inside this element, `false` otherwise.
*
* @readonly
* @type {Boolean}
*/
get isEmpty() {
return this._children.length === 0;
}
/**
* Checks whether given view tree object is of given type.
*
* Read more in {@link module:engine/view/node~Node#is}.
*
* @param {String} type
* @param {String} [name] Element name.
* @returns {Boolean}
*/
is( type, name = null ) {
if ( !name ) {
return type == 'element' || type == this.name;
} else {
return type == 'element' && name == this.name;
}
}
/**
* Clones provided element.
*
* @param {Boolean} [deep=false] If set to `true` clones element and all its children recursively. When set to `false`,
* element will be cloned without any children.
* @returns {module:engine/view/element~Element} Clone of this element.
*/
clone( deep = false ) {
const childrenClone = [];
if ( deep ) {
for ( const child of this.getChildren() ) {
childrenClone.push( child.clone( deep ) );
}
}
// ContainerElement and AttributeElement should be also cloned properly.
const cloned = new this.constructor( this.name, this._attrs, childrenClone );
// Classes and styles are cloned separately - this solution is faster than adding them back to attributes and
// parse once again in constructor.
cloned._classes = new Set( this._classes );
cloned._styles = new Map( this._styles );
// Clone custom properties.
cloned._customProperties = new Map( this._customProperties );
// Clone filler offset method.
// We can't define this method in a prototype because it's behavior which
// is changed by e.g. toWidget() function from ckeditor5-widget. Perhaps this should be one of custom props.
cloned.getFillerOffset = this.getFillerOffset;
return cloned;
}
/**
* {@link module:engine/view/element~Element#insertChildren Insert} a child node or a list of child nodes at the end of this node
* and sets the parent of these nodes to this element.
*
* @fires module:engine/view/node~Node#change
* @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} nodes Node or the list of nodes to be inserted.
* @returns {Number} Number of appended nodes.
*/
appendChildren( nodes ) {
return this.insertChildren( this.childCount, nodes );
}
/**
* Gets child at the given index.
*
* @param {Number} index Index of child.
* @returns {module:engine/view/node~Node} Child node.
*/
getChild( index ) {
return this._children[ index ];
}
/**
* Gets index of the given child node. Returns `-1` if child node is not found.
*
* @param {module:engine/view/node~Node} node Child node.
* @returns {Number} Index of the child node.
*/
getChildIndex( node ) {
return this._children.indexOf( node );
}
/**
* Gets child nodes iterator.
*
* @returns {Iterable.<module:engine/view/node~Node>} Child nodes iterator.
*/
getChildren() {
return this._children[ Symbol.iterator ]();
}
/**
* Returns an iterator that contains the keys for attributes. Order of inserting attributes is not preserved.
*
* @returns {Iterator.<String>} Keys for attributes.
*/
* getAttributeKeys() {
if ( this._classes.size > 0 ) {
yield 'class';
}
if ( this._styles.size > 0 ) {
yield 'style';
}
// This is not an optimal solution because of https://github.com/ckeditor/ckeditor5-engine/issues/454.
// It can be simplified to `yield* this._attrs.keys();`.
for ( const key of this._attrs.keys() ) {
yield key;
}
}
/**
* Returns iterator that iterates over this element's attributes.
*
* Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value.
* This format is accepted by native `Map` object and also can be passed in `Node` constructor.
*
* @returns {Iterable.<*>}
*/
* getAttributes() {
yield* this._attrs.entries();
if ( this._classes.size > 0 ) {
yield [ 'class', this.getAttribute( 'class' ) ];
}
if ( this._styles.size > 0 ) {
yield [ 'style', this.getAttribute( 'style' ) ];
}
}
/**
* Gets attribute by key. If attribute is not present - returns undefined.
*
* @param {String} key Attribute key.
* @returns {String|undefined} Attribute value.
*/
getAttribute( key ) {
if ( key == 'class' ) {
if ( this._classes.size > 0 ) {
return [ ...this._classes ].join( ' ' );
}
return undefined;
}
if ( key == 'style' ) {
if ( this._styles.size > 0 ) {
let styleString = '';
for ( const [ property, value ] of this._styles ) {
styleString += `${ property }:${ value };`;
}
return styleString;
}
return undefined;
}
return this._attrs.get( key );
}
/**
* Returns a boolean indicating whether an attribute with the specified key exists in the element.
*
* @param {String} key Attribute key.
* @returns {Boolean} `true` if attribute with the specified key exists in the element, false otherwise.
*/
hasAttribute( key ) {
if ( key == 'class' ) {
return this._classes.size > 0;
}
if ( key == 'style' ) {
return this._styles.size > 0;
}
return this._attrs.has( key );
}
/**
* Adds or overwrite attribute with a specified key and value.
*
* @param {String} key Attribute key.
* @param {String} value Attribute value.
* @fires module:engine/view/node~Node#change
*/
setAttribute( key, value ) {
this._fireChange( 'attributes', this );
if ( key == 'class' ) {
parseClasses( this._classes, value );
} else if ( key == 'style' ) {
parseInlineStyles( this._styles, value );
} else {
this._attrs.set( key, value );
}
}
/**
* Inserts a child node or a list of child nodes on the given index and sets the parent of these nodes to
* this element.
*
* @param {Number} index Position where nodes should be inserted.
* @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} nodes Node or the list of nodes to be inserted.
* @fires module:engine/view/node~Node#change
* @returns {Number} Number of inserted nodes.
*/
insertChildren( index, nodes ) {
this._fireChange( 'children', this );
let count = 0;
nodes = element_normalize( nodes );
for ( const node of nodes ) {
// If node that is being added to this element is already inside another element, first remove it from the old parent.
if ( node.parent !== null ) {
node.remove();
}
node.parent = this;
this._children.splice( index, 0, node );
index++;
count++;
}
return count;
}
/**
* Removes attribute from the element.
*
* @param {String} key Attribute key.
* @returns {Boolean} Returns true if an attribute existed and has been removed.
* @fires module:engine/view/node~Node#change
*/
removeAttribute( key ) {
this._fireChange( 'attributes', this );
// Remove class attribute.
if ( key == 'class' ) {
if ( this._classes.size > 0 ) {
this._classes.clear();
return true;
}
return false;
}
// Remove style attribute.
if ( key == 'style' ) {
if ( this._styles.size > 0 ) {
this._styles.clear();
return true;
}
return false;
}
// Remove other attributes.
return this._attrs.delete( key );
}
/**
* Removes number of child nodes starting at the given index and set the parent of these nodes to `null`.
*
* @param {Number} index Number of the first node to remove.
* @param {Number} [howMany=1] Number of nodes to remove.
* @returns {Array.<module:engine/view/node~Node>} The array of removed nodes.
* @fires module:engine/view/node~Node#change
*/
removeChildren( index, howMany = 1 ) {
this._fireChange( 'children', this );
for ( let i = index; i < index + howMany; i++ ) {
this._children[ i ].parent = null;
}
return this._children.splice( index, howMany );
}
/**
* Checks if this element is similar to other element.
* Both elements should have the same name and attributes to be considered as similar. Two similar elements
* can contain different set of children nodes.
*
* @param {module:engine/view/element~Element} otherElement
* @returns {Boolean}
*/
isSimilar( otherElement ) {
if ( !( otherElement instanceof view_element_Element ) ) {
return false;
}
// If exactly the same Element is provided - return true immediately.
if ( this === otherElement ) {
return true;
}
// Check element name.
if ( this.name != otherElement.name ) {
return false;
}
// Check number of attributes, classes and styles.
if ( this._attrs.size !== otherElement._attrs.size || this._classes.size !== otherElement._classes.size ||
this._styles.size !== otherElement._styles.size ) {
return false;
}
// Check if attributes are the same.
for ( const [ key, value ] of this._attrs ) {
if ( !otherElement._attrs.has( key ) || otherElement._attrs.get( key ) !== value ) {
return false;
}
}
// Check if classes are the same.
for ( const className of this._classes ) {
if ( !otherElement._classes.has( className ) ) {
return false;
}
}
// Check if styles are the same.
for ( const [ property, value ] of this._styles ) {
if ( !otherElement._styles.has( property ) || otherElement._styles.get( property ) !== value ) {
return false;
}
}
return true;
}
/**
* Adds specified class.
*
* element.addClass( 'foo' ); // Adds 'foo' class.
* element.addClass( 'foo', 'bar' ); // Adds 'foo' and 'bar' classes.
*
* @param {...String} className
* @fires module:engine/view/node~Node#change
*/
addClass( ...className ) {
this._fireChange( 'attributes', this );
className.forEach( name => this._classes.add( name ) );
}
/**
* Removes specified class.
*
* element.removeClass( 'foo' ); // Removes 'foo' class.
* element.removeClass( 'foo', 'bar' ); // Removes both 'foo' and 'bar' classes.
*
* @param {...String} className
* @fires module:engine/view/node~Node#change
*/
removeClass( ...className ) {
this._fireChange( 'attributes', this );
className.forEach( name => this._classes.delete( name ) );
}
/**
* Returns true if class is present.
* If more then one class is provided - returns true only when all classes are present.
*
* element.hasClass( 'foo' ); // Returns true if 'foo' class is present.
* element.hasClass( 'foo', 'bar' ); // Returns true if 'foo' and 'bar' classes are both present.
*
* @param {...String} className
*/
hasClass( ...className ) {
for ( const name of className ) {
if ( !this._classes.has( name ) ) {
return false;
}
}
return true;
}
/**
* Returns iterator that contains all class names.
*
* @returns {Iterator.<String>}
*/
getClassNames() {
return this._classes.keys();
}
/**
* Adds style to the element.
*
* element.setStyle( 'color', 'red' );
* element.setStyle( {
* color: 'red',
* position: 'fixed'
* } );
*
* @param {String|Object} property Property name or object with key - value pairs.
* @param {String} [value] Value to set. This parameter is ignored if object is provided as the first parameter.
* @fires module:engine/view/node~Node#change
*/
setStyle( property, value ) {
this._fireChange( 'attributes', this );
if ( lodash_isPlainObject( property ) ) {
const keys = Object.keys( property );
for ( const key of keys ) {
this._styles.set( key, property[ key ] );
}
} else {
this._styles.set( property, value );
}
}
/**
* Returns style value for given property.
* Undefined is returned if style does not exist.
*
* @param {String} property
* @returns {String|undefined}
*/
getStyle( property ) {
return this._styles.get( property );
}
/**
* Returns iterator that contains all style names.
*
* @returns {Iterator.<String>}
*/
getStyleNames() {
return this._styles.keys();
}
/**
* Returns true if style keys are present.
* If more then one style property is provided - returns true only when all properties are present.
*
* element.hasStyle( 'color' ); // Returns true if 'border-top' style is present.
* element.hasStyle( 'color', 'border-top' ); // Returns true if 'color' and 'border-top' styles are both present.
*
* @param {...String} property
*/
hasStyle( ...property ) {
for ( const name of property ) {
if ( !this._styles.has( name ) ) {
return false;
}
}
return true;
}
/**
* Removes specified style.
*
* element.removeStyle( 'color' ); // Removes 'color' style.
* element.removeStyle( 'color', 'border-top' ); // Removes both 'color' and 'border-top' styles.
*
* @param {...String} property
* @fires module:engine/view/node~Node#change
*/
removeStyle( ...property ) {
this._fireChange( 'attributes', this );
property.forEach( name => this._styles.delete( name ) );
}
/**
* Returns ancestor element that match specified pattern.
* Provided patterns should be compatible with {@link module:engine/view/matcher~Matcher Matcher} as it is used internally.
*
* @see module:engine/view/matcher~Matcher
* @param {Object|String|RegExp|Function} patterns Patterns used to match correct ancestor.
* See {@link module:engine/view/matcher~Matcher}.
* @returns {module:engine/view/element~Element|null} Found element or `null` if no matching ancestor was found.
*/
findAncestor( ...patterns ) {
const matcher = new Matcher( ...patterns );
let parent = this.parent;
while ( parent ) {
if ( matcher.match( parent ) ) {
return parent;
}
parent = parent.parent;
}
return null;
}
/**
* Sets a custom property. Unlike attributes, custom properties are not rendered to the DOM,
* so they can be used to add special data to elements.
*
* @param {String|Symbol} key
* @param {*} value
*/
setCustomProperty( key, value ) {
this._customProperties.set( key, value );
}
/**
* Returns the custom property value for the given key.
*
* @param {String|Symbol} key
* @returns {*}
*/
getCustomProperty( key ) {
return this._customProperties.get( key );
}
/**
* Removes the custom property stored under the given key.
*
* @param {String|Symbol} key
* @returns {Boolean} Returns true if property was removed.
*/
removeCustomProperty( key ) {
return this._customProperties.delete( key );
}
/**
* Returns an iterator which iterates over this element's custom properties.
* Iterator provides [key, value] pair for each stored property.
*
* @returns {Iterable.<*>}
*/
* getCustomProperties() {
yield* this._customProperties.entries();
}
/**
* Returns identity string based on element's name, styles, classes and other attributes.
* Two elements that {@link #isSimilar are similar} will have same identity string.
* It has the following format:
*
* 'name class="class1,class2" style="style1:value1;style2:value2" attr1="val1" attr2="val2"'
*
* For example:
*
* const element = new ViewElement( 'foo' );
* element.setAttribute( 'banana', '10' );
* element.setAttribute( 'apple', '20' );
* element.setStyle( 'color', 'red' );
* element.setStyle( 'border-color', 'white' );
* element.addClass( 'baz' );
*
* // returns 'foo class="baz" style="border-color:white;color:red" apple="20" banana="10"'
* element.getIdentity();
*
* NOTE: Classes, styles and other attributes are sorted alphabetically.
*
* @returns {String}
*/
getIdentity() {
const classes = Array.from( this._classes ).sort().join( ',' );
const styles = Array.from( this._styles ).map( i => `${ i[ 0 ] }:${ i[ 1 ] }` ).sort().join( ';' );
const attributes = Array.from( this._attrs ).map( i => `${ i[ 0 ] }="${ i[ 1 ] }"` ).sort().join( ' ' );
return this.name +
( classes == '' ? '' : ` class="${ classes }"` ) +
( styles == '' ? '' : ` style="${ styles }"` ) +
( attributes == '' ? '' : ` ${ attributes }` );
}
/**
* Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed.
*
* @abstract
* @method module:engine/view/element~Element#getFillerOffset
*/
}
// Parses inline styles and puts property - value pairs into styles map.
// Styles map is cleared before insertion.
//
// @param {Map.<String, String>} stylesMap Map to insert parsed properties and values.
// @param {String} stylesString Styles to parse.
function parseInlineStyles( stylesMap, stylesString ) {
// `null` if no quote was found in input string or last found quote was a closing quote. See below.
let quoteType = null;
let propertyNameStart = 0;
let propertyValueStart = 0;
let propertyName = null;
stylesMap.clear();
// Do not set anything if input string is empty.
if ( stylesString === '' ) {
return;
}
// Fix inline styles that do not end with `;` so they are compatible with algorithm below.
if ( stylesString.charAt( stylesString.length - 1 ) != ';' ) {
stylesString = stylesString + ';';
}
// Seek the whole string for "special characters".
for ( let i = 0; i < stylesString.length; i++ ) {
const char = stylesString.charAt( i );
if ( quoteType === null ) {
// No quote found yet or last found quote was a closing quote.
switch ( char ) {
case ':':
// Most of time colon means that property name just ended.
// Sometimes however `:` is found inside property value (for example in background image url).
if ( !propertyName ) {
// Treat this as end of property only if property name is not already saved.
// Save property name.
propertyName = stylesString.substr( propertyNameStart, i - propertyNameStart );
// Save this point as the start of property value.
propertyValueStart = i + 1;
}
break;
case '"':
case '\'':
// Opening quote found (this is an opening quote, because `quoteType` is `null`).
quoteType = char;
break;
// eslint-disable-next-line no-case-declarations
case ';':
// Property value just ended.
// Use previously stored property value start to obtain property value.
const propertyValue = stylesString.substr( propertyValueStart, i - propertyValueStart );
if ( propertyName ) {
// Save parsed part.
stylesMap.set( propertyName.trim(), propertyValue.trim() );
}
propertyName = null;
// Save this point as property name start. Property name starts immediately after previous property value ends.
propertyNameStart = i + 1;
break;
}
} else if ( char === quoteType ) {
// If a quote char is found and it is a closing quote, mark this fact by `null`-ing `quoteType`.
quoteType = null;
}
}
}
// Parses class attribute and puts all classes into classes set.
// Classes set s cleared before insertion.
//
// @param {Set.<String>} classesSet Set to insert parsed classes.
// @param {String} classesString String with classes to parse.
function parseClasses( classesSet, classesString ) {
const classArray = classesString.split( /\s+/ );
classesSet.clear();
classArray.forEach( name => classesSet.add( name ) );
}
// Converts strings to Text and non-iterables to arrays.
//
// @param {String|module:engine/view/node~Node|Iterable.<String|module:engine/view/node~Node>}
// @return {Iterable.<module:engine/view/node~Node>}
function element_normalize( nodes ) {
// Separate condition because string is iterable.
if ( typeof nodes == 'string' ) {
return [ new view_text_Text( nodes ) ];
}
if ( !isIterable( nodes ) ) {
nodes = [ nodes ];
}
// Array.from to enable .map() on non-arrays.
return Array.from( nodes )
.map( node => {
return typeof node == 'string' ? new view_text_Text( node ) : node;
} );
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/textproxy.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/view/textproxy
*/
/**
* TextProxy is a wrapper for substring of {@link module:engine/view/text~Text}. Instance of this class is created by
* {@link module:engine/view/treewalker~TreeWalker} when only a part of {@link module:engine/view/text~Text} needs to be returned.
*
* `TextProxy` has an API similar to {@link module:engine/view/text~Text Text} and allows to do most of the common tasks performed
* on view nodes.
*
* **Note:** Some `TextProxy` instances may represent whole text node, not just a part of it.
* See {@link module:engine/view/textproxy~TextProxy#isPartial}.
*
* **Note:** `TextProxy` is a readonly interface.
*
* **Note:** `TextProxy` instances are created on the fly basing on the current state of parent {@link module:engine/view/text~Text}.
* Because of this it is highly unrecommended to store references to `TextProxy instances because they might get
* invalidated due to operations on Document. Also TextProxy is not a {@link module:engine/view/node~Node} so it can not be
* inserted as a child of {@link module:engine/view/element~Element}.
*
* `TextProxy` instances are created by {@link module:engine/view/treewalker~TreeWalker view tree walker}. You should not need to create
* an instance of this class by your own.
*/
class view_textproxy_TextProxy {
/**
* Creates a text proxy.
*
* @protected
* @param {module:engine/view/text~Text} textNode Text node which part is represented by this text proxy.
* @param {Number} offsetInText Offset in {@link module:engine/view/textproxy~TextProxy#textNode text node}
* from which the text proxy starts.
* @param {Number} length Text proxy length, that is how many text node's characters, starting from `offsetInText` it represents.
* @constructor
*/
constructor( textNode, offsetInText, length ) {
/**
* Reference to the {@link module:engine/view/text~Text} element which TextProxy is a substring.
*
* @readonly
* @member {module:engine/view/text~Text} module:engine/view/textproxy~TextProxy#textNode
*/
this.textNode = textNode;
if ( offsetInText < 0 || offsetInText > textNode.data.length ) {
/**
* Given offsetInText value is incorrect.
*
* @error view-textproxy-wrong-offsetintext
*/
throw new CKEditorError( 'view-textproxy-wrong-offsetintext: Given offsetInText value is incorrect.' );
}
if ( length < 0 || offsetInText + length > textNode.data.length ) {
/**
* Given length value is incorrect.
*
* @error view-textproxy-wrong-length
*/
throw new CKEditorError( 'view-textproxy-wrong-length: Given length value is incorrect.' );
}
/**
* Text data represented by this text proxy.
*
* @readonly
* @member {String} module:engine/view/textproxy~TextProxy#data
*/
this.data = textNode.data.substring( offsetInText, offsetInText + length );
/**
* Offset in the `textNode` where this `TextProxy` instance starts.
*
* @readonly
* @member {Number} module:engine/view/textproxy~TextProxy#offsetInText
*/
this.offsetInText = offsetInText;
}
/**
* Flag indicating whether `TextProxy` instance covers only part of the original {@link module:engine/view/text~Text text node}
* (`true`) or the whole text node (`false`).
*
* This is `false` when text proxy starts at the very beginning of {@link module:engine/view/textproxy~TextProxy#textNode textNode}
* ({@link module:engine/view/textproxy~TextProxy#offsetInText offsetInText} equals `0`) and text proxy sizes is equal to
* text node size.
*
* @readonly
* @type {Boolean}
*/
get isPartial() {
return this.data.length !== this.textNode.data.length;
}
/**
* Parent of this text proxy, which is same as parent of text node represented by this text proxy.
*
* @readonly
* @type {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment|null}
*/
get parent() {
return this.textNode.parent;
}
/**
* Root of this text proxy, which is same as root of text node represented by this text proxy.
*
* @readonly
* @type {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment}
*/
get root() {
return this.textNode.root;
}
/**
* {@link module:engine/view/document~Document View document} that owns this text proxy, or `null` if the text proxy is inside
* {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
*
* @readonly
* @type {module:engine/view/document~Document|null}
*/
get document() {
return this.textNode.document;
}
/**
* Checks whether given view tree object is of given type.
*
* Read more in {@link module:engine/view/node~Node#is}.
*
* @param {String} type
* @returns {Boolean}
*/
is( type ) {
return type == 'textProxy';
}
/**
* Returns ancestors array of this text proxy.
*
* @param {Object} options Options object.
* @param {Boolean} [options.includeSelf=false] When set to `true` {#textNode} will be also included in parent's array.
* @param {Boolean} [options.parentFirst=false] When set to `true`, array will be sorted from text proxy parent to
* root element, otherwise root element will be the first item in the array.
* @returns {Array} Array with ancestors.
*/
getAncestors( options = { includeSelf: false, parentFirst: false } ) {
const ancestors = [];
let parent = options.includeSelf ? this.textNode : this.parent;
while ( parent !== null ) {
ancestors[ options.parentFirst ? 'push' : 'unshift' ]( parent );
parent = parent.parent;
}
return ancestors;
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/treewalker.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/view/treewalker
*/
/**
* Position iterator class. It allows to iterate forward and backward over the document.
*/
class view_treewalker_TreeWalker {
/**
* Creates a range iterator. All parameters are optional, but you have to specify either `boundaries` or `startPosition`.
*
* @constructor
* @param {Object} options Object with configuration.
* @param {module:engine/view/range~Range} [options.boundaries=null] Range to define boundaries of the iterator.
* @param {module:engine/view/position~Position} [options.startPosition] Starting position.
* @param {'forward'|'backward'} [options.direction='forward'] Walking direction.
* @param {Boolean} [options.singleCharacters=false] Flag indicating whether all characters from
* {@link module:engine/view/text~Text} should be returned as one {@link module:engine/view/text~Text} (`false`) ore one by one as
* {@link module:engine/view/textproxy~TextProxy} (`true`).
* @param {Boolean} [options.shallow=false] Flag indicating whether iterator should enter elements or not. If the
* iterator is shallow child nodes of any iterated node will not be returned along with `elementEnd` tag.
* @param {Boolean} [options.ignoreElementEnd=false] Flag indicating whether iterator should ignore `elementEnd`
* tags. If the option is true walker will not return a parent node of start position. If this option is `true`
* each {@link module:engine/view/element~Element} will be returned once, while if the option is `false` they might be returned
* twice: for `'elementStart'` and `'elementEnd'`.
*/
constructor( options = {} ) {
if ( !options.boundaries && !options.startPosition ) {
/**
* Neither boundaries nor starting position have been defined.
*
* @error view-tree-walker-no-start-position
*/
throw new CKEditorError( 'view-tree-walker-no-start-position: Neither boundaries nor starting position have been defined.' );
}
if ( options.direction && options.direction != 'forward' && options.direction != 'backward' ) {
throw new CKEditorError(
'view-tree-walker-unknown-direction: Only `backward` and `forward` direction allowed.',
{ direction: options.direction }
);
}
/**
* Iterator boundaries.
*
* When the iterator is walking `'forward'` on the end of boundary or is walking `'backward'`
* on the start of boundary, then `{ done: true }` is returned.
*
* If boundaries are not defined they are set before first and after last child of the root node.
*
* @readonly
* @member {module:engine/view/range~Range} module:engine/view/treewalker~TreeWalker#boundaries
*/
this.boundaries = options.boundaries || null;
/**
* Iterator position. If start position is not defined then position depends on {@link #direction}. If direction is
* `'forward'` position starts form the beginning, when direction is `'backward'` position starts from the end.
*
* @readonly
* @member {module:engine/view/position~Position} module:engine/view/treewalker~TreeWalker#position
*/
if ( options.startPosition ) {
this.position = view_position_Position.createFromPosition( options.startPosition );
} else {
this.position = view_position_Position.createFromPosition( options.boundaries[ options.direction == 'backward' ? 'end' : 'start' ] );
}
/**
* Walking direction. Defaults `'forward'`.
*
* @readonly
* @member {'backward'|'forward'} module:engine/view/treewalker~TreeWalker#direction
*/
this.direction = options.direction || 'forward';
/**
* Flag indicating whether all characters from {@link module:engine/view/text~Text} should be returned as one
* {@link module:engine/view/text~Text} or one by one as {@link module:engine/view/textproxy~TextProxy}.
*
* @readonly
* @member {Boolean} module:engine/view/treewalker~TreeWalker#singleCharacters
*/
this.singleCharacters = !!options.singleCharacters;
/**
* Flag indicating whether iterator should enter elements or not. If the iterator is shallow child nodes of any
* iterated node will not be returned along with `elementEnd` tag.
*
* @readonly
* @member {Boolean} module:engine/view/treewalker~TreeWalker#shallow
*/
this.shallow = !!options.shallow;
/**
* Flag indicating whether iterator should ignore `elementEnd` tags. If set to `true`, walker will not
* return a parent node of the start position. Each {@link module:engine/view/element~Element} will be returned once.
* When set to `false` each element might be returned twice: for `'elementStart'` and `'elementEnd'`.
*
* @readonly
* @member {Boolean} module:engine/view/treewalker~TreeWalker#ignoreElementEnd
*/
this.ignoreElementEnd = !!options.ignoreElementEnd;
/**
* Start boundary parent.
*
* @private
* @member {module:engine/view/node~Node} module:engine/view/treewalker~TreeWalker#_boundaryStartParent
*/
this._boundaryStartParent = this.boundaries ? this.boundaries.start.parent : null;
/**
* End boundary parent.
*
* @private
* @member {module:engine/view/node~Node} module:engine/view/treewalker~TreeWalker#_boundaryEndParent
*/
this._boundaryEndParent = this.boundaries ? this.boundaries.end.parent : null;
}
/**
* Iterator interface.
*/
[ Symbol.iterator ]() {
return this;
}
/**
* Moves {@link #position} in the {@link #direction} skipping values as long as the callback function returns `true`.
*
* For example:
*
* walker.skip( value => value.type == 'text' ); // <p>{}foo</p> -> <p>foo[]</p>
* walker.skip( value => true ); // Move the position to the end: <p>{}foo</p> -> <p>foo</p>[]
* walker.skip( value => false ); // Do not move the position.
*
* @param {Function} skip Callback function. Gets {@link module:engine/view/treewalker~TreeWalkerValue} and should
* return `true` if the value should be skipped or `false` if not.
*/
skip( skip ) {
let done, value, prevPosition;
do {
prevPosition = this.position;
( { done, value } = this.next() );
} while ( !done && skip( value ) );
if ( !done ) {
this.position = prevPosition;
}
}
/**
* Iterator interface method.
* Detects walking direction and makes step forward or backward.
*
* @returns {Object} Object implementing iterator interface, returning information about taken step.
*/
next() {
if ( this.direction == 'forward' ) {
return this._next();
} else {
return this._previous();
}
}
/**
* Makes a step forward in view. Moves the {@link #position} to the next position and returns the encountered value.
*
* @private
* @returns {Object}
* @returns {Boolean} return.done `true` if iterator is done, `false` otherwise.
* @returns {module:engine/view/treewalker~TreeWalkerValue} return.value Information about taken step.
*/
_next() {
let position = view_position_Position.createFromPosition( this.position );
const previousPosition = this.position;
const parent = position.parent;
// We are at the end of the root.
if ( parent.parent === null && position.offset === parent.childCount ) {
return { done: true };
}
// We reached the walker boundary.
if ( parent === this._boundaryEndParent && position.offset == this.boundaries.end.offset ) {
return { done: true };
}
// Get node just after current position.
let node;
// Text is a specific parent because it contains string instead of child nodes.
if ( parent instanceof view_text_Text ) {
if ( position.isAtEnd ) {
// Prevent returning "elementEnd" for Text node. Skip that value and return the next walker step.
this.position = view_position_Position.createAfter( parent );
return this._next();
}
node = parent.data[ position.offset ];
} else {
node = parent.getChild( position.offset );
}
if ( node instanceof view_element_Element ) {
if ( !this.shallow ) {
position = new view_position_Position( node, 0 );
} else {
position.offset++;
}
this.position = position;
return this._formatReturnValue( 'elementStart', node, previousPosition, position, 1 );
} else if ( node instanceof view_text_Text ) {
if ( this.singleCharacters ) {
position = new view_position_Position( node, 0 );
this.position = position;
return this._next();
} else {
let charactersCount = node.data.length;
let item = node;
// If text stick out of walker range, we need to cut it and wrap by TextProxy.
if ( node == this._boundaryEndParent ) {
charactersCount = this.boundaries.end.offset;
item = new view_textproxy_TextProxy( node, 0, charactersCount );
position = view_position_Position.createAfter( item );
} else {
// If not just keep moving forward.
position.offset++;
}
this.position = position;
return this._formatReturnValue( 'text', item, previousPosition, position, charactersCount );
}
} else if ( typeof node == 'string' ) {
let textLength;
if ( this.singleCharacters ) {
textLength = 1;
} else {
// Check if text stick out of walker range.
const endOffset = parent === this._boundaryEndParent ? this.boundaries.end.offset : parent.data.length;
textLength = endOffset - position.offset;
}
const textProxy = new view_textproxy_TextProxy( parent, position.offset, textLength );
position.offset += textLength;
this.position = position;
return this._formatReturnValue( 'text', textProxy, previousPosition, position, textLength );
} else {
// `node` is not set, we reached the end of current `parent`.
position = view_position_Position.createAfter( parent );
this.position = position;
if ( this.ignoreElementEnd ) {
return this._next();
} else {
return this._formatReturnValue( 'elementEnd', parent, previousPosition, position );
}
}
}
/**
* Makes a step backward in view. Moves the {@link #position} to the previous position and returns the encountered value.
*
* @private
* @returns {Object}
* @returns {Boolean} return.done True if iterator is done.
* @returns {module:engine/view/treewalker~TreeWalkerValue} return.value Information about taken step.
*/
_previous() {
let position = view_position_Position.createFromPosition( this.position );
const previousPosition = this.position;
const parent = position.parent;
// We are at the beginning of the root.
if ( parent.parent === null && position.offset === 0 ) {
return { done: true };
}
// We reached the walker boundary.
if ( parent == this._boundaryStartParent && position.offset == this.boundaries.start.offset ) {
return { done: true };
}
// Get node just before current position.
let node;
// Text {@link module:engine/view/text~Text} element is a specific parent because contains string instead of child nodes.
if ( parent instanceof view_text_Text ) {
if ( position.isAtStart ) {
// Prevent returning "elementStart" for Text node. Skip that value and return the next walker step.
this.position = view_position_Position.createBefore( parent );
return this._previous();
}
node = parent.data[ position.offset - 1 ];
} else {
node = parent.getChild( position.offset - 1 );
}
if ( node instanceof view_element_Element ) {
if ( !this.shallow ) {
position = new view_position_Position( node, node.childCount );
this.position = position;
if ( this.ignoreElementEnd ) {
return this._previous();
} else {
return this._formatReturnValue( 'elementEnd', node, previousPosition, position );
}
} else {
position.offset--;
this.position = position;
return this._formatReturnValue( 'elementStart', node, previousPosition, position, 1 );
}
} else if ( node instanceof view_text_Text ) {
if ( this.singleCharacters ) {
position = new view_position_Position( node, node.data.length );
this.position = position;
return this._previous();
} else {
let charactersCount = node.data.length;
let item = node;
// If text stick out of walker range, we need to cut it and wrap by TextProxy.
if ( node == this._boundaryStartParent ) {
const offset = this.boundaries.start.offset;
item = new view_textproxy_TextProxy( node, offset, node.data.length - offset );
charactersCount = item.data.length;
position = view_position_Position.createBefore( item );
} else {
// If not just keep moving backward.
position.offset--;
}
this.position = position;
return this._formatReturnValue( 'text', item, previousPosition, position, charactersCount );
}
} else if ( typeof node == 'string' ) {
let textLength;
if ( !this.singleCharacters ) {
// Check if text stick out of walker range.
const startOffset = parent === this._boundaryStartParent ? this.boundaries.start.offset : 0;
textLength = position.offset - startOffset;
} else {
textLength = 1;
}
position.offset -= textLength;
const textProxy = new view_textproxy_TextProxy( parent, position.offset, textLength );
this.position = position;
return this._formatReturnValue( 'text', textProxy, previousPosition, position, textLength );
} else {
// `node` is not set, we reached the beginning of current `parent`.
position = view_position_Position.createBefore( parent );
this.position = position;
return this._formatReturnValue( 'elementStart', parent, previousPosition, position, 1 );
}
}
/**
* Format returned data and adjust `previousPosition` and `nextPosition` if reach the bound of the {@link module:engine/view/text~Text}.
*
* @private
* @param {module:engine/view/treewalker~TreeWalkerValueType} type Type of step.
* @param {module:engine/view/item~Item} item Item between old and new position.
* @param {module:engine/view/position~Position} previousPosition Previous position of iterator.
* @param {module:engine/view/position~Position} nextPosition Next position of iterator.
* @param {Number} [length] Length of the item.
* @returns {module:engine/view/treewalker~TreeWalkerValue}
*/
_formatReturnValue( type, item, previousPosition, nextPosition, length ) {
// Text is a specific parent, because contains string instead of children.
// Walker doesn't enter to the Text except situations when walker is iterating over every single character,
// or the bound starts/ends inside the Text. So when the position is at the beginning or at the end of the Text
// we move it just before or just after Text.
if ( item instanceof view_textproxy_TextProxy ) {
// Position is at the end of Text.
if ( item.offsetInText + item.data.length == item.textNode.data.length ) {
if ( this.direction == 'forward' && !( this.boundaries && this.boundaries.end.isEqual( this.position ) ) ) {
nextPosition = view_position_Position.createAfter( item.textNode );
// When we change nextPosition of returned value we need also update walker current position.
this.position = nextPosition;
} else {
previousPosition = view_position_Position.createAfter( item.textNode );
}
}
// Position is at the begining ot the text.
if ( item.offsetInText === 0 ) {
if ( this.direction == 'backward' && !( this.boundaries && this.boundaries.start.isEqual( this.position ) ) ) {
nextPosition = view_position_Position.createBefore( item.textNode );
// When we change nextPosition of returned value we need also update walker current position.
this.position = nextPosition;
} else {
previousPosition = view_position_Position.createBefore( item.textNode );
}
}
}
return {
done: false,
value: {
type,
item,
previousPosition,
nextPosition,
length
}
};
}
}
/**
* Type of the step made by {@link module:engine/view/treewalker~TreeWalker}.
* Possible values: `'elementStart'` if walker is at the beginning of a node, `'elementEnd'` if walker is at the end
* of node, or `'text'` if walker traversed over single and multiple characters.
* For {@link module:engine/view/text~Text} `elementStart` and `elementEnd` is not returned.
*
* @typedef {String} module:engine/view/treewalker~TreeWalkerValueType
*/
/**
* Object returned by {@link module:engine/view/treewalker~TreeWalker} when traversing tree view.
*
* @typedef {Object} module:engine/view/treewalker~TreeWalkerValue
* @property {module:engine/view/treewalker~TreeWalkerValueType} type
* @property {module:engine/view/item~Item} item Item between old and new positions of {@link module:engine/view/treewalker~TreeWalker}.
* @property {module:engine/view/position~Position} previousPosition Previous position of the iterator.
* * Forward iteration: For `'elementEnd'` it is the last position inside the element. For all other types it is the
* position before the item. Note that it is more efficient to use this position then calculate the position before
* the node using {@link module:engine/view/position~Position.createBefore}.
* * Backward iteration: For `'elementStart'` it is the first position inside the element. For all other types it is
* the position after item.
* * If the position is at the beginning or at the end of the {@link module:engine/view/text~Text} it is always moved from the
* inside of the Text to its parent just before or just after Text.
* @property {module:engine/view/position~Position} nextPosition Next position of the iterator.
* * Forward iteration: For `'elementStart'` it is the first position inside the element. For all other types it is
* the position after the item.
* * Backward iteration: For `'elementEnd'` it is last position inside element. For all other types it is the position
* before the item.
* * If the position is at the beginning or at the end of the {@link module:engine/view/text~Text} it is always moved from the
* inside of the Text to its parent just before or just after Text.
* @property {Number} [length] Length of the item. For `'elementStart'` it is 1. For `'text'` it is
* the length of the text. For `'elementEnd'` it is undefined.
*/
/**
* Tree walking directions.
*
* @typedef {'forward'|'backward'} module:engine/view/treewalker~TreeWalkerDirection
*/
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/containerelement.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/view/containerelement
*/
/**
* Containers are elements which define document structure. They define boundaries for
* {@link module:engine/view/attributeelement~AttributeElement attributes}. They are mostly use for block elements like `<p>` or `<div>`.
*
* Editing engine does not define fixed HTML DTD. This is why the type of the {@link module:engine/view/element~Element} need to
* be defined by the feature developer.
*
* Creating an element you should use `ContainerElement` class or {@link module:engine/view/attributeelement~AttributeElement}. This is
* important to define the type of the element because of two reasons:
*
* Firstly, {@link module:engine/view/domconverter~DomConverter} needs the information what is an editable block to convert elements to
* DOM properly. {@link module:engine/view/domconverter~DomConverter} will ensure that `ContainerElement` is editable and it is possible
* to put caret inside it, even if the container is empty.
*
* Secondly, {@link module:engine/view/writer~writer view writer} uses this information.
* Nodes {@link module:engine/view/writer~writer.breakAttributes breaking} and {@link module:engine/view/writer~writer.mergeAttributes
* merging}
* is performed only in a bounds of a container nodes.
*
* For instance if `<p>` is an container and `<b>` is attribute:
*
* <p><b>fo^o</b></p>
*
* {@link module:engine/view/writer~writer.breakAttributes breakAttributes} will create:
*
* <p><b>fo</b><b>o</b></p>
*
* There might be a need to mark `<span>` element as a container node, for example in situation when it will be a
* container of an inline widget:
*
* <span color="red">foobar</span> // attribute
* <span data-widget>foobar</span> // container
*
* @extends module:engine/view/element~Element
*/
class ContainerElement extends view_element_Element {
/**
* Creates a container element.
*
* @see module:engine/view/element~Element
*/
constructor( name, attrs, children ) {
super( name, attrs, children );
/**
* Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed.
*
* @method #getFillerOffset
* @returns {Number|null} Block filler offset or `null` if block filler is not needed.
*/
this.getFillerOffset = getFillerOffset;
}
/**
* @inheritDoc
*/
is( type, name = null ) {
if ( !name ) {
return type == 'containerElement' || super.is( type );
} else {
return ( type == 'containerElement' && name == this.name ) || super.is( type, name );
}
}
}
// Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed.
//
// @returns {Number|null} Block filler offset or `null` if block filler is not needed.
function getFillerOffset() {
for ( const child of this.getChildren() ) {
// If there's any non-UI element – don't render the bogus.
if ( !child.is( 'uiElement' ) ) {
return null;
}
}
// If there are only UI elements – render the bogus at the end of the element.
return this.childCount;
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/editableelement.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/view/editableelement
*/
const documentSymbol = Symbol( 'document' );
/**
* Editable element which can be a {@link module:engine/view/rooteditableelement~RootEditableElement root}
* or nested editable area in the editor.
*
* Editable is automatically read-only when its {module:engine/view/document~Document Document} is read-only.
*
* @extends module:engine/view/containerelement~ContainerElement
* @mixes module:utils/observablemixin~ObservableMixin
*/
class editableelement_EditableElement extends ContainerElement {
/**
* Creates an editable element.
*/
constructor( name, attrs, children ) {
super( name, attrs, children );
/**
* Whether the editable is in read-write or read-only mode.
*
* @observable
* @member {Boolean} module:engine/view/editableelement~EditableElement#isReadOnly
*/
this.set( 'isReadOnly', false );
/**
* Whether the editable is focused.
*
* This property updates when {@link module:engine/view/document~Document#isFocused document.isFocused} is changed and after each
* {@link module:engine/view/document~Document#render render} method call.
*
* @readonly
* @observable
* @member {Boolean} module:engine/view/editableelement~EditableElement#isFocused
*/
this.set( 'isFocused', false );
/**
* The {@link module:engine/view/document~Document} which is an owner of this root.
* Can only by set once.
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-editableelement-document-already-set`
* when document is already set.
*
* @member {module:engine/view/document~Document} #document
*/
}
get document() {
return this.getCustomProperty( documentSymbol );
}
set document( document ) {
if ( this.getCustomProperty( documentSymbol ) ) {
/**
* View document is already set. It can only be set once.
*
* @error view-editableelement-document-already-set
*/
throw new CKEditorError( 'view-editableelement-document-already-set: View document is already set.' );
}
this.setCustomProperty( documentSymbol, document );
this.bind( 'isReadOnly' ).to( document );
this.bind( 'isFocused' ).to(
document,
'isFocused',
isFocused => isFocused && document.selection.editableElement == this
);
// Update focus state before each rendering. Rendering should not change neither the selection nor the value of
// document.isFocused property.
this.listenTo( document, 'render', () => {
this.isFocused = document.isFocused && document.selection.editableElement == this;
}, { priority: 'high' } );
}
}
mix( editableelement_EditableElement, observablemixin );
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/position.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/view/position
*/
/**
* Position in the tree. Position is always located before or after a node.
*/
class view_position_Position {
/**
* Creates a position.
*
* @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} parent Position parent.
* @param {Number} offset Position offset.
*/
constructor( parent, offset ) {
/**
* Position parent.
*
* @member {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment}
* module:engine/view/position~Position#parent
*/
this.parent = parent;
/**
* Position offset.
*
* @member {Number} module:engine/view/position~Position#offset
*/
this.offset = offset;
}
/**
* Node directly after the position. Equals `null` when there is no node after position or position is located
* inside text node.
*
* @readonly
* @type {module:engine/view/node~Node|null}
*/
get nodeAfter() {
if ( this.parent.is( 'text' ) ) {
return null;
}
return this.parent.getChild( this.offset ) || null;
}
/**
* Node directly before the position. Equals `null` when there is no node before position or position is located
* inside text node.
*
* @readonly
* @type {module:engine/view/node~Node|null}
*/
get nodeBefore() {
if ( this.parent.is( 'text' ) ) {
return null;
}
return this.parent.getChild( this.offset - 1 ) || null;
}
/**
* Is `true` if position is at the beginning of its {@link module:engine/view/position~Position#parent parent}, `false` otherwise.
*
* @readonly
* @type {Boolean}
*/
get isAtStart() {
return this.offset === 0;
}
/**
* Is `true` if position is at the end of its {@link module:engine/view/position~Position#parent parent}, `false` otherwise.
*
* @readonly
* @type {Boolean}
*/
get isAtEnd() {
const endOffset = this.parent.is( 'text' ) ? this.parent.data.length : this.parent.childCount;
return this.offset === endOffset;
}
/**
* Position's root, that is the root of the position's parent element.
*
* @readonly
* @type {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment}
*/
get root() {
return this.parent.root;
}
/**
* {@link module:engine/view/editableelement~EditableElement EditableElement} instance that contains this position, or `null` if
* position is not inside an editable element.
*
* @type {module:engine/view/editableelement~EditableElement|null}
*/
get editableElement() {
let editable = this.parent;
while ( !( editable instanceof editableelement_EditableElement ) ) {
if ( editable.parent ) {
editable = editable.parent;
} else {
return null;
}
}
return editable;
}
/**
* Returns a new instance of Position with offset incremented by `shift` value.
*
* @param {Number} shift How position offset should get changed. Accepts negative values.
* @returns {module:engine/view/position~Position} Shifted position.
*/
getShiftedBy( shift ) {
const shifted = view_position_Position.createFromPosition( this );
const offset = shifted.offset + shift;
shifted.offset = offset < 0 ? 0 : offset;
return shifted;
}
/**
* Gets the farthest position which matches the callback using
* {@link module:engine/view/treewalker~TreeWalker TreeWalker}.
*
* For example:
*
* getLastMatchingPosition( value => value.type == 'text' ); // <p>{}foo</p> -> <p>foo[]</p>
* getLastMatchingPosition( value => value.type == 'text', { direction: 'backward' } ); // <p>foo[]</p> -> <p>{}foo</p>
* getLastMatchingPosition( value => false ); // Do not move the position.
*
* @param {Function} skip Callback function. Gets {@link module:engine/view/treewalker~TreeWalkerValue} and should
* return `true` if the value should be skipped or `false` if not.
* @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}.
*
* @returns {module:engine/view/position~Position} The position after the last item which matches the `skip` callback test.
*/
getLastMatchingPosition( skip, options = {} ) {
options.startPosition = this;
const treeWalker = new view_treewalker_TreeWalker( options );
treeWalker.skip( skip );
return treeWalker.position;
}
/**
* Returns ancestors array of this position, that is this position's parent and it's ancestors.
*
* @returns {Array} Array with ancestors.
*/
getAncestors() {
if ( this.parent.is( 'documentFragment' ) ) {
return [ this.parent ];
} else {
return this.parent.getAncestors( { includeSelf: true } );
}
}
/**
* Returns a {@link module:engine/view/node~Node} or {@link module:engine/view/documentfragment~DocumentFragment}
* which is a common ancestor of both positions.
*
* @param {module:engine/view/position~Position} position
* @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null}
*/
getCommonAncestor( position ) {
const ancestorsA = this.getAncestors();
const ancestorsB = position.getAncestors();
let i = 0;
while ( ancestorsA[ i ] == ancestorsB[ i ] && ancestorsA[ i ] ) {
i++;
}
return i === 0 ? null : ancestorsA[ i - 1 ];
}
/**
* Checks whether this position equals given position.
*
* @param {module:engine/view/position~Position} otherPosition Position to compare with.
* @returns {Boolean} True if positions are same.
*/
isEqual( otherPosition ) {
return ( this.parent == otherPosition.parent && this.offset == otherPosition.offset );
}
/**
* Checks whether this position is located before given position. When method returns `false` it does not mean that
* this position is after give one. Two positions may be located inside separate roots and in that situation this
* method will still return `false`.
*
* @see module:engine/view/position~Position#isAfter
* @see module:engine/view/position~Position#compareWith
* @param {module:engine/view/position~Position} otherPosition Position to compare with.
* @returns {Boolean} Returns `true` if this position is before given position.
*/
isBefore( otherPosition ) {
return this.compareWith( otherPosition ) == 'before';
}
/**
* Checks whether this position is located after given position. When method returns `false` it does not mean that
* this position is before give one. Two positions may be located inside separate roots and in that situation this
* method will still return `false`.
*
* @see module:engine/view/position~Position#isBefore
* @see module:engine/view/position~Position#compareWith
* @param {module:engine/view/position~Position} otherPosition Position to compare with.
* @returns {Boolean} Returns `true` if this position is after given position.
*/
isAfter( otherPosition ) {
return this.compareWith( otherPosition ) == 'after';
}
/**
* Checks whether this position is before, after or in same position that other position. Two positions may be also
* different when they are located in separate roots.
*
* @param {module:engine/view/position~Position} otherPosition Position to compare with.
* @returns {module:engine/view/position~PositionRelation}
*/
compareWith( otherPosition ) {
if ( this.isEqual( otherPosition ) ) {
return 'same';
}
// If positions have same parent.
if ( this.parent === otherPosition.parent ) {
return this.offset - otherPosition.offset < 0 ? 'before' : 'after';
}
// Get path from root to position's parent element.
const path = this.getAncestors();
const otherPath = otherPosition.getAncestors();
// Compare both path arrays to find common ancestor.
const result = compareArrays( path, otherPath );
let commonAncestorIndex;
switch ( result ) {
case 0:
// No common ancestors found.
return 'different';
case 'prefix':
commonAncestorIndex = path.length - 1;
break;
case 'extension':
commonAncestorIndex = otherPath.length - 1;
break;
default:
commonAncestorIndex = result - 1;
}
// Common ancestor of two positions.
const commonAncestor = path[ commonAncestorIndex ];
const nextAncestor1 = path[ commonAncestorIndex + 1 ];
const nextAncestor2 = otherPath[ commonAncestorIndex + 1 ];
// Check if common ancestor is not one of the parents.
if ( commonAncestor === this.parent ) {
const index = this.offset - nextAncestor2.index;
return index <= 0 ? 'before' : 'after';
} else if ( commonAncestor === otherPosition.parent ) {
const index = nextAncestor1.index - otherPosition.offset;
return index < 0 ? 'before' : 'after';
}
const index = nextAncestor1.index - nextAncestor2.index;
// Compare indexes of next ancestors inside common one.
return index < 0 ? 'before' : 'after';
}
/**
* Creates position at the given location. The location can be specified as:
*
* * a {@link module:engine/view/position~Position position},
* * parent element and offset (offset defaults to `0`),
* * parent element and `'end'` (sets position at the end of that element),
* * {@link module:engine/view/item~Item view item} and `'before'` or `'after'` (sets position before or after given view item).
*
* This method is a shortcut to other constructors such as:
*
* * {@link module:engine/view/position~Position.createBefore},
* * {@link module:engine/view/position~Position.createAfter},
* * {@link module:engine/view/position~Position.createFromPosition}.
*
* @param {module:engine/view/item~Item|module:engine/model/position~Position} itemOrPosition
* @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
* first parameter is a {@link module:engine/view/item~Item view item}.
*/
static createAt( itemOrPosition, offset ) {
if ( itemOrPosition instanceof view_position_Position ) {
return this.createFromPosition( itemOrPosition );
} else {
const node = itemOrPosition;
if ( offset == 'end' ) {
offset = node.is( 'text' ) ? node.data.length : node.childCount;
} else if ( offset == 'before' ) {
return this.createBefore( node );
} else if ( offset == 'after' ) {
return this.createAfter( node );
} else if ( !offset ) {
offset = 0;
}
return new view_position_Position( node, offset );
}
}
/**
* Creates a new position after given view item.
*
* @param {module:engine/view/item~Item} item View item after which the position should be located.
* @returns {module:engine/view/position~Position}
*/
static createAfter( item ) {
// TextProxy is not a instance of Node so we need do handle it in specific way.
if ( item.is( 'textProxy' ) ) {
return new view_position_Position( item.textNode, item.offsetInText + item.data.length );
}
if ( !item.parent ) {
/**
* You can not make a position after a root.
*
* @error view-position-after-root
* @param {module:engine/view/node~Node} root
*/
throw new CKEditorError( 'view-position-after-root: You can not make position after root.', { root: item } );
}
return new view_position_Position( item.parent, item.index + 1 );
}
/**
* Creates a new position before given view item.
*
* @param {module:engine/view/item~Item} item View item before which the position should be located.
* @returns {module:engine/view/position~Position}
*/
static createBefore( item ) {
// TextProxy is not a instance of Node so we need do handle it in specific way.
if ( item.is( 'textProxy' ) ) {
return new view_position_Position( item.textNode, item.offsetInText );
}
if ( !item.parent ) {
/**
* You cannot make a position before a root.
*
* @error view-position-before-root
* @param {module:engine/view/node~Node} root
*/
throw new CKEditorError( 'view-position-before-root: You can not make position before root.', { root: item } );
}
return new view_position_Position( item.parent, item.index );
}
/**
* Creates and returns a new instance of `Position`, which is equal to the passed position.
*
* @param {module:engine/view/position~Position} position Position to be cloned.
* @returns {module:engine/view/position~Position}
*/
static createFromPosition( position ) {
return new this( position.parent, position.offset );
}
}
/**
* A flag indicating whether this position is `'before'` or `'after'` or `'same'` as given position.
* If positions are in different roots `'different'` flag is returned.
*
* @typedef {String} module:engine/view/position~PositionRelation
*/
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/range.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/view/range
*/
/**
* Tree view range.
*/
class view_range_Range {
/**
* Creates a range spanning from `start` position to `end` position.
*
* **Note:** Constructor creates it's own {@link module:engine/view/position~Position} instances basing on passed values.
*
* @param {module:engine/view/position~Position} start Start position.
* @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
*/
constructor( start, end = null ) {
/**
* Start position.
*
* @member {module:engine/view/position~Position}
*/
this.start = view_position_Position.createFromPosition( start );
/**
* End position.
*
* @member {module:engine/view/position~Position}
*/
this.end = end ? view_position_Position.createFromPosition( end ) : view_position_Position.createFromPosition( start );
}
/**
* Returns an iterator that iterates over all {@link module:engine/view/item~Item view items} that are in this range and returns
* them together with additional information like length or {@link module:engine/view/position~Position positions},
* grouped as {@link module:engine/view/treewalker~TreeWalkerValue}.
*
* This iterator uses {@link module:engine/view/treewalker~TreeWalker TreeWalker} with `boundaries` set to this range and
* `ignoreElementEnd` option
* set to `true`.
*
* @returns {Iterable.<module:engine/view/treewalker~TreeWalkerValue>}
*/
* [ Symbol.iterator ]() {
yield* new view_treewalker_TreeWalker( { boundaries: this, ignoreElementEnd: true } );
}
/**
* Returns whether the range is collapsed, that is it start and end positions are equal.
*
* @type {Boolean}
*/
get isCollapsed() {
return this.start.isEqual( this.end );
}
/**
* Returns whether this range is flat, that is if {@link module:engine/view/range~Range#start start} position and
* {@link module:engine/view/range~Range#end end} position are in the same {@link module:engine/view/position~Position#parent parent}.
*
* @type {Boolean}
*/
get isFlat() {
return this.start.parent === this.end.parent;
}
/**
* Range root element.
*
* @type {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment}
*/
get root() {
return this.start.root;
}
/**
* Creates a maximal range that has the same content as this range but is expanded in both ways (at the beginning
* and at the end).
*
* For example:
*
* <p>Foo</p><p><b>{Bar}</b></p> -> <p>Foo</p>[<p><b>Bar</b>]</p>
* <p><b>foo</b>{bar}<span></span></p> -> <p><b>foo[</b>bar<span></span>]</p>
*
* Note that in the sample above:
* - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement},
* - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement},
* - `<span>` have type of {@link module:engine/view/uielement~UIElement}.
*
* @returns {module:engine/view/range~Range} Enlarged range.
*/
getEnlarged() {
let start = this.start.getLastMatchingPosition( enlargeTrimSkip, { direction: 'backward' } );
let end = this.end.getLastMatchingPosition( enlargeTrimSkip );
// Fix positions, in case if they are in Text node.
if ( start.parent.is( 'text' ) && start.isAtStart ) {
start = view_position_Position.createBefore( start.parent );
}
if ( end.parent.is( 'text' ) && end.isAtEnd ) {
end = view_position_Position.createAfter( end.parent );
}
return new view_range_Range( start, end );
}
/**
* Creates a minimum range that has the same content as this range but is trimmed in both ways (at the beginning
* and at the end).
*
* For example:
*
* <p>Foo</p>[<p><b>Bar</b>]</p> -> <p>Foo</p><p><b>{Bar}</b></p>
* <p><b>foo[</b>bar<span></span>]</p> -> <p><b>foo</b>{bar}<span></span></p>
*
* Note that in the sample above:
* - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement},
* - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement},
* - `<span>` have type of {@link module:engine/view/uielement~UIElement}.
*
* @returns {module:engine/view/range~Range} Shrink range.
*/
getTrimmed() {
let start = this.start.getLastMatchingPosition( enlargeTrimSkip );
if ( start.isAfter( this.end ) || start.isEqual( this.end ) ) {
return new view_range_Range( start, start );
}
let end = this.end.getLastMatchingPosition( enlargeTrimSkip, { direction: 'backward' } );
const nodeAfterStart = start.nodeAfter;
const nodeBeforeEnd = end.nodeBefore;
// Because TreeWalker prefers positions next to text node, we need to move them manually into these text nodes.
if ( nodeAfterStart && nodeAfterStart.is( 'text' ) ) {
start = new view_position_Position( nodeAfterStart, 0 );
}
if ( nodeBeforeEnd && nodeBeforeEnd.is( 'text' ) ) {
end = new view_position_Position( nodeBeforeEnd, nodeBeforeEnd.data.length );
}
return new view_range_Range( start, end );
}
/**
* Two ranges are equal if their start and end positions are equal.
*
* @param {module:engine/view/range~Range} otherRange Range to compare with.
* @returns {Boolean} `true` if ranges are equal, `false` otherwise
*/
isEqual( otherRange ) {
return this == otherRange || ( this.start.isEqual( otherRange.start ) && this.end.isEqual( otherRange.end ) );
}
/**
* Checks whether this range contains given {@link module:engine/view/position~Position position}.
*
* @param {module:engine/view/position~Position} position Position to check.
* @returns {Boolean} `true` if given {@link module:engine/view/position~Position position} is contained in this range,
* `false` otherwise.
*/
containsPosition( position ) {
return position.isAfter( this.start ) && position.isBefore( this.end );
}
/**
* Checks whether this range contains given {@link module:engine/view/range~Range range}.
*
* @param {module:engine/view/range~Range} otherRange Range to check.
* @param {Boolean} [loose=false] Whether the check is loose or strict. If the check is strict (`false`), compared range cannot
* start or end at the same position as this range boundaries. If the check is loose (`true`), compared range can start, end or
* even be equal to this range. Note that collapsed ranges are always compared in strict mode.
* @returns {Boolean} `true` if given {@link module:engine/view/range~Range range} boundaries are contained by this range, `false`
* otherwise.
*/
containsRange( otherRange, loose = false ) {
if ( otherRange.isCollapsed ) {
loose = false;
}
const containsStart = this.containsPosition( otherRange.start ) || ( loose && this.start.isEqual( otherRange.start ) );
const containsEnd = this.containsPosition( otherRange.end ) || ( loose && this.end.isEqual( otherRange.end ) );
return containsStart && containsEnd;
}
/**
* Computes which part(s) of this {@link module:engine/view/range~Range range} is not a part of given
* {@link module:engine/view/range~Range range}.
* Returned array contains zero, one or two {@link module:engine/view/range~Range ranges}.
*
* Examples:
*
* let foo = new Text( 'foo' );
* let img = new ContainerElement( 'img' );
* let bar = new Text( 'bar' );
* let p = new ContainerElement( 'p', null, [ foo, img, bar ] );
*
* let range = new Range( new Position( foo, 2 ), new Position( bar, 1 ); // "o", img, "b" are in range.
* let otherRange = new Range( new Position( foo, 1 ), new Position( bar, 2 ); "oo", img, "ba" are in range.
* let transformed = range.getDifference( otherRange );
* // transformed array has no ranges because `otherRange` contains `range`
*
* otherRange = new Range( new Position( foo, 1 ), new Position( p, 2 ); // "oo", img are in range.
* transformed = range.getDifference( otherRange );
* // transformed array has one range: from ( p, 2 ) to ( bar, 1 )
*
* otherRange = new Range( new Position( p, 1 ), new Position( p, 2 ) ); // img is in range.
* transformed = range.getDifference( otherRange );
* // transformed array has two ranges: from ( foo, 1 ) to ( p, 1 ) and from ( p, 2 ) to ( bar, 1 )
*
* @param {module:engine/view/range~Range} otherRange Range to differentiate against.
* @returns {Array.<module:engine/view/range~Range>} The difference between ranges.
*/
getDifference( otherRange ) {
const ranges = [];
if ( this.isIntersecting( otherRange ) ) {
// Ranges intersect.
if ( this.containsPosition( otherRange.start ) ) {
// Given range start is inside this range. This means that we have to
// add shrunken range - from the start to the middle of this range.
ranges.push( new view_range_Range( this.start, otherRange.start ) );
}
if ( this.containsPosition( otherRange.end ) ) {
// Given range end is inside this range. This means that we have to
// add shrunken range - from the middle of this range to the end.
ranges.push( new view_range_Range( otherRange.end, this.end ) );
}
} else {
// Ranges do not intersect, return the original range.
ranges.push( view_range_Range.createFromRange( this ) );
}
return ranges;
}
/**
* Returns an intersection of this {@link module:engine/view/range~Range range} and given {@link module:engine/view/range~Range range}.
* Intersection is a common part of both of those ranges. If ranges has no common part, returns `null`.
*
* Examples:
*
* let foo = new Text( 'foo' );
* let img = new ContainerElement( 'img' );
* let bar = new Text( 'bar' );
* let p = new ContainerElement( 'p', null, [ foo, img, bar ] );
*
* let range = new Range( new Position( foo, 2 ), new Position( bar, 1 ); // "o", img, "b" are in range.
* let otherRange = new Range( new Position( foo, 1 ), new Position( p, 2 ); // "oo", img are in range.
* let transformed = range.getIntersection( otherRange ); // range from ( foo, 1 ) to ( p, 2 ).
*
* otherRange = new Range( new Position( bar, 1 ), new Position( bar, 3 ); "ar" is in range.
* transformed = range.getIntersection( otherRange ); // null - no common part.
*
* @param {module:engine/view/range~Range} otherRange Range to check for intersection.
* @returns {module:engine/view/range~Range|null} A common part of given ranges or `null` if ranges have no common part.
*/
getIntersection( otherRange ) {
if ( this.isIntersecting( otherRange ) ) {
// Ranges intersect, so a common range will be returned.
// At most, it will be same as this range.
let commonRangeStart = this.start;
let commonRangeEnd = this.end;
if ( this.containsPosition( otherRange.start ) ) {
// Given range start is inside this range. This means thaNt we have to
// shrink common range to the given range start.
commonRangeStart = otherRange.start;
}
if ( this.containsPosition( otherRange.end ) ) {
// Given range end is inside this range. This means that we have to
// shrink common range to the given range end.
commonRangeEnd = otherRange.end;
}
return new view_range_Range( commonRangeStart, commonRangeEnd );
}
// Ranges do not intersect, so they do not have common part.
return null;
}
/**
* Creates a {@link module:engine/view/treewalker~TreeWalker TreeWalker} instance with this range as a boundary.
*
* @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}.
* @param {module:engine/view/position~Position} [options.startPosition]
* @param {Boolean} [options.singleCharacters=false]
* @param {Boolean} [options.shallow=false]
* @param {Boolean} [options.ignoreElementEnd=false]
*/
getWalker( options = {} ) {
options.boundaries = this;
return new view_treewalker_TreeWalker( options );
}
/**
* Returns a {@link module:engine/view/node~Node} or {@link module:engine/view/documentfragment~DocumentFragment}
* which is a common ancestor of range's both ends (in which the entire range is contained).
*
* @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null}
*/
getCommonAncestor() {
return this.start.getCommonAncestor( this.end );
}
/**
* Returns an iterator that iterates over all {@link module:engine/view/item~Item view items} that are in this range and returns
* them.
*
* This method uses {@link module:engine/view/treewalker~TreeWalker} with `boundaries` set to this range and `ignoreElementEnd` option
* set to `true`. However it returns only {@link module:engine/view/item~Item items},
* not {@link module:engine/view/treewalker~TreeWalkerValue}.
*
* You may specify additional options for the tree walker. See {@link module:engine/view/treewalker~TreeWalker} for
* a full list of available options.
*
* @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}.
* @returns {Iterable.<module:engine/view/item~Item>}
*/
* getItems( options = {} ) {
options.boundaries = this;
options.ignoreElementEnd = true;
const treeWalker = new view_treewalker_TreeWalker( options );
for ( const value of treeWalker ) {
yield value.item;
}
}
/**
* Returns an iterator that iterates over all {@link module:engine/view/position~Position positions} that are boundaries or
* contained in this range.
*
* This method uses {@link module:engine/view/treewalker~TreeWalker} with `boundaries` set to this range. However it returns only
* {@link module:engine/view/position~Position positions}, not {@link module:engine/view/treewalker~TreeWalkerValue}.
*
* You may specify additional options for the tree walker. See {@link module:engine/view/treewalker~TreeWalker} for
* a full list of available options.
*
* @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}.
* @returns {Iterable.<module:engine/view/position~Position>}
*/
* getPositions( options = {} ) {
options.boundaries = this;
const treeWalker = new view_treewalker_TreeWalker( options );
yield treeWalker.position;
for ( const value of treeWalker ) {
yield value.nextPosition;
}
}
/**
* Checks and returns whether this range intersects with given range.
*
* @param {module:engine/view/range~Range} otherRange Range to compare with.
* @returns {Boolean} True if ranges intersect.
*/
isIntersecting( otherRange ) {
return this.start.isBefore( otherRange.end ) && this.end.isAfter( otherRange.start );
}
/**
* Creates a range from given parents and offsets.
*
* @param {module:engine/view/element~Element} startElement Start position parent element.
* @param {Number} startOffset Start position offset.
* @param {module:engine/view/element~Element} endElement End position parent element.
* @param {Number} endOffset End position offset.
* @returns {module:engine/view/range~Range} Created range.
*/
static createFromParentsAndOffsets( startElement, startOffset, endElement, endOffset ) {
return new this(
new view_position_Position( startElement, startOffset ),
new view_position_Position( endElement, endOffset )
);
}
/**
* Creates and returns a new instance of Range which is equal to passed range.
*
* @param {module:engine/view/range~Range} range Range to clone.
* @returns {module:engine/view/range~Range}
*/
static createFromRange( range ) {
return new this( range.start, range.end );
}
/**
* Creates a new range, spreading from specified {@link module:engine/view/position~Position position} to a position moved by
* given `shift`. If `shift` is a negative value, shifted position is treated as the beginning of the range.
*
* @param {module:engine/view/position~Position} position Beginning of the range.
* @param {Number} shift How long the range should be.
* @returns {module:engine/view/range~Range}
*/
static createFromPositionAndShift( position, shift ) {
const start = position;
const end = position.getShiftedBy( shift );
return shift > 0 ? new this( start, end ) : new this( end, start );
}
/**
* Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
* that element and ends after the last child of that element.
*
* @param {module:engine/view/element~Element} element Element which is a parent for the range.
* @returns {module:engine/view/range~Range}
*/
static createIn( element ) {
return this.createFromParentsAndOffsets( element, 0, element, element.childCount );
}
/**
* Creates a range that starts before given {@link module:engine/view/item~Item view item} and ends after it.
*
* @param {module:engine/view/item~Item} item
* @returns {module:engine/view/range~Range}
*/
static createOn( item ) {
return this.createFromPositionAndShift( view_position_Position.createBefore( item ), 1 );
}
/**
* Creates a collapsed range at given {@link module:engine/view/position~Position position}
* or on the given {@link module:engine/view/item~Item item}.
*
* @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
* @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
* first parameter is a {@link module:engine/view/item~Item view item}.
*/
static createCollapsedAt( itemOrPosition, offset ) {
const start = view_position_Position.createAt( itemOrPosition, offset );
const end = view_position_Position.createFromPosition( start );
return new view_range_Range( start, end );
}
}
// Function used by getEnlarged and getTrimmed methods.
function enlargeTrimSkip( value ) {
if ( value.item.is( 'attributeElement' ) || value.item.is( 'uiElement' ) ) {
return true;
}
return false;
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/conversion/mapper.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/conversion/mapper
*/
/**
* Maps elements and positions between {@link module:engine/view/document~Document view} and {@link module:engine/model/model model}.
*
* Mapper use bound elements to find corresponding elements and positions, so, to get proper results,
* all model elements should be {@link module:engine/conversion/mapper~Mapper#bindElements bound}.
*
* To map complex model to/from view relations, you may provide custom callbacks for
* {@link module:engine/conversion/mapper~Mapper#event:modelToViewPosition modelToViewPosition event} and
* {@link module:engine/conversion/mapper~Mapper#event:viewToModelPosition viewToModelPosition event} that are fired whenever
* a position mapping request occurs.
* Those events are fired by {@link module:engine/conversion/mapper~Mapper#toViewPosition toViewPosition}
* and {@link module:engine/conversion/mapper~Mapper#toModelPosition toModelPosition} methods. `Mapper` adds it's own default callbacks
* with `'lowest'` priority. To override default `Mapper` mapping, add custom callback with higher priority and
* stop the event.
*/
class mapper_Mapper {
/**
* Creates an instance of the mapper.
*/
constructor() {
/**
* Model element to view element mapping.
*
* @private
* @member {WeakMap}
*/
this._modelToViewMapping = new WeakMap();
/**
* View element to model element mapping.
*
* @private
* @member {WeakMap}
*/
this._viewToModelMapping = new WeakMap();
/**
* A map containing callbacks between view element names and functions evaluating length of view elements
* in model.
*
* @private
* @member {Map}
*/
this._viewToModelLengthCallbacks = new Map();
// Default mapper algorithm for mapping model position to view position.
this.on( 'modelToViewPosition', ( evt, data ) => {
if ( data.viewPosition ) {
return;
}
const viewContainer = this._modelToViewMapping.get( data.modelPosition.parent );
data.viewPosition = this._findPositionIn( viewContainer, data.modelPosition.offset );
}, { priority: 'low' } );
// Default mapper algorithm for mapping view position to model position.
this.on( 'viewToModelPosition', ( evt, data ) => {
if ( data.modelPosition ) {
return;
}
let viewBlock = data.viewPosition.parent;
let modelParent = this._viewToModelMapping.get( viewBlock );
while ( !modelParent ) {
viewBlock = viewBlock.parent;
modelParent = this._viewToModelMapping.get( viewBlock );
}
const modelOffset = this._toModelOffset( data.viewPosition.parent, data.viewPosition.offset, viewBlock );
data.modelPosition = position_Position.createFromParentAndOffset( modelParent, modelOffset );
}, { priority: 'low' } );
}
/**
* Marks model and view elements as corresponding. Corresponding elements can be retrieved by using
* the {@link module:engine/conversion/mapper~Mapper#toModelElement toModelElement} and
* {@link module:engine/conversion/mapper~Mapper#toViewElement toViewElement} methods.
* The information that elements are bound is also used to translate positions.
*
* @param {module:engine/model/element~Element} modelElement Model element.
* @param {module:engine/view/element~Element} viewElement View element.
*/
bindElements( modelElement, viewElement ) {
this._modelToViewMapping.set( modelElement, viewElement );
this._viewToModelMapping.set( viewElement, modelElement );
}
/**
* Unbinds given {@link module:engine/view/element~Element view element} from the map.
*
* @param {module:engine/view/element~Element} viewElement View element to unbind.
*/
unbindViewElement( viewElement ) {
const modelElement = this.toModelElement( viewElement );
this._unbindElements( modelElement, viewElement );
}
/**
* Unbinds given {@link module:engine/model/element~Element model element} from the map.
*
* @param {module:engine/model/element~Element} modelElement Model element to unbind.
*/
unbindModelElement( modelElement ) {
const viewElement = this.toViewElement( modelElement );
this._unbindElements( modelElement, viewElement );
}
/**
* Removes all model to view and view to model bindings.
*/
clearBindings() {
this._modelToViewMapping = new WeakMap();
this._viewToModelMapping = new WeakMap();
}
/**
* Gets the corresponding model element.
*
* **Note:** {@link module:engine/view/uielement~UIElement} does not have corresponding element in model.
*
* @param {module:engine/view/element~Element} viewElement View element.
* @returns {module:engine/model/element~Element|undefined} Corresponding model element or `undefined` if not found.
*/
toModelElement( viewElement ) {
return this._viewToModelMapping.get( viewElement );
}
/**
* Gets the corresponding view element.
*
* @param {module:engine/model/element~Element} modelElement Model element.
* @returns {module:engine/view/element~Element|undefined} Corresponding view element or `undefined` if not found.
*/
toViewElement( modelElement ) {
return this._modelToViewMapping.get( modelElement );
}
/**
* Gets the corresponding model range.
*
* @param {module:engine/view/range~Range} viewRange View range.
* @returns {module:engine/model/range~Range} Corresponding model range.
*/
toModelRange( viewRange ) {
return new range_Range( this.toModelPosition( viewRange.start ), this.toModelPosition( viewRange.end ) );
}
/**
* Gets the corresponding view range.
*
* @param {module:engine/model/range~Range} modelRange Model range.
* @returns {module:engine/view/range~Range} Corresponding view range.
*/
toViewRange( modelRange ) {
return new view_range_Range( this.toViewPosition( modelRange.start ), this.toViewPosition( modelRange.end ) );
}
/**
* Gets the corresponding model position.
*
* @fires viewToModelPosition
* @param {module:engine/view/position~Position} viewPosition View position.
* @returns {module:engine/model/position~Position} Corresponding model position.
*/
toModelPosition( viewPosition ) {
const data = {
viewPosition,
mapper: this
};
this.fire( 'viewToModelPosition', data );
return data.modelPosition;
}
/**
* Gets the corresponding view position.
*
* @fires modelToViewPosition
* @param {module:engine/model/position~Position} modelPosition Model position.
* @returns {module:engine/view/position~Position} Corresponding view position.
*/
toViewPosition( modelPosition ) {
const data = {
modelPosition,
mapper: this
};
this.fire( 'modelToViewPosition', data );
return data.viewPosition;
}
/**
* Registers a callback that evaluates the length in the model of a view element with given name.
*
* The callback is fired with one argument, which is a view element instance. The callback is expected to return
* a number representing the length of view element in model.
*
* // List item in view may contain nested list, which have other list items. In model though,
* // the lists are represented by flat structure. Because of those differences, length of list view element
* // may be greater than one. In the callback it's checked how many nested list items are in evaluated list item.
*
* function getViewListItemLength( element ) {
* let length = 1;
*
* for ( let child of element.getChildren() ) {
* if ( child.name == 'ul' || child.name == 'ol' ) {
* for ( let item of child.getChildren() ) {
* length += getViewListItemLength( item );
* }
* }
* }
*
* return length;
* }
*
* mapper.registerViewToModelLength( 'li', getViewListItemLength );
*
* @param {String} viewElementName Name of view element for which callback is registered.
* @param {Function} lengthCallback Function return a length of view element instance in model.
*/
registerViewToModelLength( viewElementName, lengthCallback ) {
this._viewToModelLengthCallbacks.set( viewElementName, lengthCallback );
}
/**
* Calculates model offset based on the view position and the block element.
*
* Example:
*
* <p>foo<b>ba|r</b></p> // _toModelOffset( b, 2, p ) -> 5
*
* Is a sum of:
*
* <p>foo|<b>bar</b></p> // _toModelOffset( p, 3, p ) -> 3
* <p>foo<b>ba|r</b></p> // _toModelOffset( b, 2, b ) -> 2
*
* @private
* @param {module:engine/view/element~Element} viewParent Position parent.
* @param {Number} viewOffset Position offset.
* @param {module:engine/view/element~Element} viewBlock Block used as a base to calculate offset.
* @returns {Number} Offset in the model.
*/
_toModelOffset( viewParent, viewOffset, viewBlock ) {
if ( viewBlock != viewParent ) {
// See example.
const offsetToParentStart = this._toModelOffset( viewParent.parent, viewParent.index, viewBlock );
const offsetInParent = this._toModelOffset( viewParent, viewOffset, viewParent );
return offsetToParentStart + offsetInParent;
}
// viewBlock == viewParent, so we need to calculate the offset in the parent element.
// If the position is a text it is simple ("ba|r" -> 2).
if ( viewParent.is( 'text' ) ) {
return viewOffset;
}
// If the position is in an element we need to sum lengths of siblings ( <b> bar </b> foo | -> 3 + 3 = 6 ).
let modelOffset = 0;
for ( let i = 0; i < viewOffset; i++ ) {
modelOffset += this.getModelLength( viewParent.getChild( i ) );
}
return modelOffset;
}
/**
* Removes binding between given elements.
*
* @private
* @param {module:engine/model/element~Element} modelElement Model element to unbind.
* @param {module:engine/view/element~Element} viewElement View element to unbind.
*/
_unbindElements( modelElement, viewElement ) {
this._viewToModelMapping.delete( viewElement );
this._modelToViewMapping.delete( modelElement );
}
/**
* Gets the length of the view element in the model.
*
* The length is calculated as follows:
* * if {@link #registerViewToModelLength length mapping callback} is provided for given `viewNode` it is used to
* evaluate model length (`viewNode` is used as first and only parameter passed to the callback),
* * length of a {@link module:engine/view/text~Text text node} is equal to the length of it's
* {@link module:engine/view/text~Text#data data},
* * length of a {@link module:engine/view/uielement~UIElement ui element} is equal to 0,
* * length of a mapped {@link module:engine/view/element~Element element} is equal to 1,
* * length of a not-mapped {@link module:engine/view/element~Element element} is equal to the length of it's children.
*
* Examples:
*
* foo -> 3 // Text length is equal to it's data length.
* <p>foo</p> -> 1 // Length of an element which is mapped is by default equal to 1.
* <b>foo</b> -> 3 // Length of an element which is not mapped is a length of its children.
* <div><p>x</p><p>y</p></div> -> 2 // Assuming that <div> is not mapped and <p> are mapped.
*
* @param {module:engine/view/element~Element} viewNode View node.
* @returns {Number} Length of the node in the tree model.
*/
getModelLength( viewNode ) {
if ( this._viewToModelLengthCallbacks.get( viewNode.name ) ) {
const callback = this._viewToModelLengthCallbacks.get( viewNode.name );
return callback( viewNode );
} else if ( this._viewToModelMapping.has( viewNode ) ) {
return 1;
} else if ( viewNode.is( 'text' ) ) {
return viewNode.data.length;
} else if ( viewNode.is( 'uiElement' ) ) {
return 0;
} else {
let len = 0;
for ( const child of viewNode.getChildren() ) {
len += this.getModelLength( child );
}
return len;
}
}
/**
* Finds the position in the view node (or its children) with the expected model offset.
*
* Example:
*
* <p>fo<b>bar</b>bom</p> -> expected offset: 4
*
* _findPositionIn( p, 4 ):
* <p>|fo<b>bar</b>bom</p> -> expected offset: 4, actual offset: 0
* <p>fo|<b>bar</b>bom</p> -> expected offset: 4, actual offset: 2
* <p>fo<b>bar</b>|bom</p> -> expected offset: 4, actual offset: 5 -> we are too far
*
* _findPositionIn( b, 4 - ( 5 - 3 ) ):
* <p>fo<b>|bar</b>bom</p> -> expected offset: 2, actual offset: 0
* <p>fo<b>bar|</b>bom</p> -> expected offset: 2, actual offset: 3 -> we are too far
*
* _findPositionIn( bar, 2 - ( 3 - 3 ) ):
* We are in the text node so we can simple find the offset.
* <p>fo<b>ba|r</b>bom</p> -> expected offset: 2, actual offset: 2 -> position found
*
* @private
* @param {module:engine/view/element~Element} viewParent Tree view element in which we are looking for the position.
* @param {Number} expectedOffset Expected offset.
* @returns {module:engine/view/position~Position} Found position.
*/
_findPositionIn( viewParent, expectedOffset ) {
// Last scanned view node.
let viewNode;
// Length of the last scanned view node.
let lastLength = 0;
let modelOffset = 0;
let viewOffset = 0;
// In the text node it is simple: offset in the model equals offset in the text.
if ( viewParent.is( 'text' ) ) {
return new view_position_Position( viewParent, expectedOffset );
}
// In other cases we add lengths of child nodes to find the proper offset.
// If it is smaller we add the length.
while ( modelOffset < expectedOffset ) {
viewNode = viewParent.getChild( viewOffset );
lastLength = this.getModelLength( viewNode );
modelOffset += lastLength;
viewOffset++;
}
// If it equals we found the position.
if ( modelOffset == expectedOffset ) {
return this._moveViewPositionToTextNode( new view_position_Position( viewParent, viewOffset ) );
}
// If it is higher we need to enter last child.
else {
// ( modelOffset - lastLength ) is the offset to the child we enter,
// so we subtract it from the expected offset to fine the offset in the child.
return this._findPositionIn( viewNode, expectedOffset - ( modelOffset - lastLength ) );
}
}
/**
* Because we prefer positions in text nodes over positions next to text node moves view position to the text node
* if it was next to it.
*
* <p>[]<b>foo</b></p> -> <p>[]<b>foo</b></p> // do not touch if position is not directly next to text
* <p>foo[]<b>foo</b></p> -> <p>foo{}<b>foo</b></p> // move to text node
* <p><b>[]foo</b></p> -> <p><b>{}foo</b></p> // move to text node
*
* @private
* @param {module:engine/view/position~Position} viewPosition Position potentially next to text node.
* @returns {module:engine/view/position~Position} Position in text node if possible.
*/
_moveViewPositionToTextNode( viewPosition ) {
// If the position is just after text node, put it at the end of that text node.
// If the position is just before text node, put it at the beginning of that text node.
const nodeBefore = viewPosition.nodeBefore;
const nodeAfter = viewPosition.nodeAfter;
if ( nodeBefore instanceof view_text_Text ) {
return new view_position_Position( nodeBefore, nodeBefore.data.length );
} else if ( nodeAfter instanceof view_text_Text ) {
return new view_position_Position( nodeAfter, 0 );
}
// Otherwise, just return the given position.
return viewPosition;
}
/**
* Fired for each model-to-view position mapping request. The purpose of this event is to enable custom model-to-view position
* mapping. Callbacks added to this event take {@link module:engine/model/position~Position model position} and are expected to
* calculate {@link module:engine/view/position~Position view position}. Calculated view position should be added as `viewPosition`
* value in `data` object that is passed as one of parameters to the event callback.
*
* // Assume that "captionedImage" model element is converted to <img> and following <span> elements in view,
* // and the model element is bound to <img> element. Force mapping model positions inside "captionedImage" to that
* // <span> element.
* mapper.on( 'modelToViewPosition', ( evt, data ) => {
* const positionParent = modelPosition.parent;
*
* if ( positionParent.name == 'captionedImage' ) {
* const viewImg = data.mapper.toViewElement( positionParent );
* const viewCaption = viewImg.nextSibling; // The <span> element.
*
* data.viewPosition = new ViewPosition( viewCaption, modelPosition.offset );
*
* // Stop the event if other callbacks should not modify calculated value.
* evt.stop();
* }
* } );
*
* **Note:** keep in mind that custom callback provided for this event should use provided `data.modelPosition` only to check
* what is before the position (or position's parent). This is important when model-to-view position mapping is used in
* remove change conversion. Model after the removed position (that is being mapped) does not correspond to view, so it cannot be used:
*
* // Incorrect:
* const modelElement = data.modelPosition.nodeAfter;
* const viewElement = data.mapper.toViewElement( modelElement );
* // ... Do something with `viewElement` and set `data.viewPosition`.
*
* // Correct:
* const prevModelElement = data.modelPosition.nodeBefore;
* const prevViewElement = data.mapper.toViewElement( prevModelElement );
* // ... Use `prevViewElement` to find correct `data.viewPosition`.
*
* **Note:** default mapping callback is provided with `low` priority setting and does not cancel the event, so it is possible to
* attach a custom callback after default callback and also use `data.viewPosition` calculated by default callback
* (for example to fix it).
*
* **Note:** default mapping callback will not fire if `data.viewPosition` is already set.
*
* **Note:** these callbacks are called **very often**. For efficiency reasons, it is advised to use them only when position
* mapping between given model and view elements is unsolvable using just elements mapping and default algorithm. Also,
* the condition that checks if special case scenario happened should be as simple as possible.
*
* @event modelToViewPosition
* @param {Object} data Data pipeline object that can store and pass data between callbacks. The callback should add
* `viewPosition` value to that object with calculated {@link module:engine/view/position~Position view position}.
* @param {module:engine/conversion/mapper~Mapper} data.mapper Mapper instance that fired the event.
*/
/**
* Fired for each view-to-model position mapping request. See {@link module:engine/conversion/mapper~Mapper#event:modelToViewPosition}.
*
* // See example in `modelToViewPosition` event description.
* // This custom mapping will map positions from <span> element next to <img> to the "captionedImage" element.
* mapper.on( 'viewToModelPosition', ( evt, data ) => {
* const positionParent = viewPosition.parent;
*
* if ( positionParent.hasClass( 'image-caption' ) ) {
* const viewImg = positionParent.previousSibling;
* const modelImg = data.mapper.toModelElement( viewImg );
*
* data.modelPosition = new ModelPosition( modelImg, viewPosition.offset );
* evt.stop();
* }
* } );
*
* **Note:** default mapping callback is provided with `low` priority setting and does not cancel the event, so it is possible to
* attach a custom callback after default callback and also use `data.modelPosition` calculated by default callback
* (for example to fix it).
*
* **Note:** default mapping callback will not fire if `data.modelPosition` is already set.
*
* **Note:** these callbacks are called **very often**. For efficiency reasons, it is advised to use them only when position
* mapping between given model and view elements is unsolvable using just elements mapping and default algorithm. Also,
* the condition that checks if special case scenario happened should be as simple as possible.
*
* @event viewToModelPosition
* @param {Object} data Data pipeline object that can store and pass data between callbacks. The callback should add
* `modelPosition` value to that object with calculated {@link module:engine/model/position~Position model position}.
* @param {module:engine/conversion/mapper~Mapper} data.mapper Mapper instance that fired the event.
*/
}
mix( mapper_Mapper, emittermixin );
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/conversion/modelconsumable.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/conversion/modelconsumable
*/
/**
* Manages a list of consumable values for {@link module:engine/model/item~Item model items}.
*
* Consumables are various aspects of the model. A model item can be broken down into singular properties that might be
* taken into consideration when converting that item.
*
* `ModelConsumable` is used by {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher} while analyzing changed
* parts of {@link module:engine/model/document~Document the document}. The added / changed / removed model items are broken down
* into singular properties (the item itself and it's attributes). All those parts are saved in `ModelConsumable`. Then,
* during conversion, when given part of model item is converted (i.e. the view element has been inserted into the view,
* but without attributes), consumable value is removed from `ModelConsumable`.
*
* For model items, `ModelConsumable` stores consumable values of one of following types: `insert`, `addAttribute:<attributeKey>`,
* `changeAttribute:<attributeKey>`, `removeAttribute:<attributeKey>`.
*
* In most cases, it is enough to let {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}
* gather consumable values, so there is no need to use
* @link module:engine/conversion/modelconsumable~ModelConsumable#add add method} directly.
* However, it is important to understand how consumable values can be
* {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed}.
* See {@link module:engine/conversion/model-selection-to-view-converters default model to view converters} for more information.
*
* Keep in mind, that one conversion event may have multiple callbacks (converters) attached to it. Each of those is
* able to convert one or more parts of the model. However, when one of those callbacks actually converts
* something, other should not, because they would duplicate the results. Using `ModelConsumable` helps avoiding
* this situation, because callbacks should only convert those values, which were not yet consumed from `ModelConsumable`.
*
* Consuming multiple values in a single callback:
*
* // Converter for custom `image` element that might have a `caption` element inside which changes
* // how the image is displayed in the view:
* //
* // Model:
* //
* // [image]
* // └─ [caption]
* // └─ foo
* //
* // View:
* //
* // <figure>
* // ├─ <img />
* // └─ <caption>
* // └─ foo
* modelConversionDispatcher.on( 'insert:image', ( evt, data, consumable, conversionApi ) => {
* // First, consume the `image` element.
* consumable.consume( data.item, 'insert' );
*
* // Just create normal image element for the view.
* // Maybe it will be "decorated" later.
* const viewImage = new ViewElement( 'img' );
* const insertPosition = conversionApi.mapper.toViewPosition( data.range.start );
*
* // Check if the `image` element has children.
* if ( data.item.childCount > 0 ) {
* const modelCaption = data.item.getChild( 0 );
*
* // `modelCaption` insertion change is consumed from consumable values.
* // It will not be converted by other converters, but it's children (probably some text) will be.
* // Through mapping, converters for text will know where to insert contents of `modelCaption`.
* if ( consumable.consume( modelCaption, 'insert' ) ) {
* const viewCaption = new ViewElement( 'figcaption' );
*
* const viewImageHolder = new ViewElement( 'figure', null, [ viewImage, viewCaption ] );
*
* conversionApi.mapper.bindElements( modelCaption, viewCaption );
* conversionApi.mapper.bindElements( data.item, viewImageHolder );
* viewWriter.insert( insertPosition, viewImageHolder );
* }
* } else {
* conversionApi.mapper.bindElements( data.item, viewImage );
* viewWriter.insert( insertPosition, viewImage );
* }
*
* evt.stop();
* } );
*/
class modelconsumable_ModelConsumable {
/**
* Creates an empty consumables list.
*/
constructor() {
/**
* Contains list of consumable values.
*
* @private
* @member {Map} module:engine/conversion/modelconsumable~ModelConsumable#_consumable
*/
this._consumable = new Map();
/**
* For each {@link module:engine/model/textproxy~TextProxy} added to `ModelConsumable`, this registry holds parent
* of that `TextProxy` and start and end indices of that `TextProxy`. This allows identification of `TextProxy`
* instances that points to the same part of the model but are different instances. Each distinct `TextProxy`
* is given unique `Symbol` which is then registered as consumable. This process is transparent for `ModelConsumable`
* API user because whenever `TextProxy` is added, tested, consumed or reverted, internal mechanisms of
* `ModelConsumable` translates `TextProxy` to that unique `Symbol`.
*
* @private
* @member {Map} module:engine/conversion/modelconsumable~ModelConsumable#_textProxyRegistry
*/
this._textProxyRegistry = new Map();
}
/**
* Adds a consumable value to the consumables list and links it with given model item.
*
* modelConsumable.add( modelElement, 'insert' ); // Add `modelElement` insertion change to consumable values.
* modelConsumable.add( modelElement, 'addAttribute:bold' ); // Add `bold` attribute insertion on `modelElement` change.
* modelConsumable.add( modelElement, 'removeAttribute:bold' ); // Add `bold` attribute removal on `modelElement` change.
* modelConsumable.add( modelSelection, 'selection' ); // Add `modelSelection` to consumable values.
* modelConsumable.add( modelSelection, 'selectionAttribute:bold' ); // Add `bold` attribute on `modelSelection` to consumables.
* modelConsumable.add( modelRange, 'range' ); // Add `modelRange` to consumable values.
*
* @param {module:engine/model/item~Item|module:engine/model/selection~Selection|module:engine/model/range~Range} item
* Model item, range or selection that has the consumable.
* @param {String} type Consumable type.
*/
add( item, type ) {
if ( item instanceof textproxy_TextProxy ) {
item = this._getSymbolForTextProxy( item );
}
if ( !this._consumable.has( item ) ) {
this._consumable.set( item, new Map() );
}
this._consumable.get( item ).set( type, true );
}
/**
* Removes given consumable value from given model item.
*
* modelConsumable.consume( modelElement, 'insert' ); // Remove `modelElement` insertion change from consumable values.
* modelConsumable.consume( modelElement, 'addAttribute:bold' ); // Remove `bold` attribute insertion on `modelElement` change.
* modelConsumable.consume( modelElement, 'removeAttribute:bold' ); // Remove `bold` attribute removal on `modelElement` change.
* modelConsumable.consume( modelSelection, 'selection' ); // Remove `modelSelection` from consumable values.
* modelConsumable.consume( modelSelection, 'selectionAttribute:bold' ); // Remove `bold` on `modelSelection` from consumables.
* modelConsumable.consume( modelRange, 'range' ); // Remove 'modelRange' from consumable values.
*
* @param {module:engine/model/item~Item|module:engine/model/selection~Selection|module:engine/model/range~Range} item
* Model item, range or selection from which consumable will be consumed.
* @param {String} type Consumable type.
* @returns {Boolean} `true` if consumable value was available and was consumed, `false` otherwise.
*/
consume( item, type ) {
if ( item instanceof textproxy_TextProxy ) {
item = this._getSymbolForTextProxy( item );
}
if ( this.test( item, type ) ) {
this._consumable.get( item ).set( type, false );
return true;
} else {
return false;
}
}
/**
* Tests whether there is a consumable value of given type connected with given model item.
*
* modelConsumable.test( modelElement, 'insert' ); // Check for `modelElement` insertion change.
* modelConsumable.test( modelElement, 'addAttribute:bold' ); // Check for `bold` attribute insertion on `modelElement` change.
* modelConsumable.test( modelElement, 'removeAttribute:bold' ); // Check for `bold` attribute removal on `modelElement` change.
* modelConsumable.test( modelSelection, 'selection' ); // Check if `modelSelection` is consumable.
* modelConsumable.test( modelSelection, 'selectionAttribute:bold' ); // Check if `bold` on `modelSelection` is consumable.
* modelConsumable.test( modelRange, 'range' ); // Check if `modelRange` is consumable.
*
* @param {module:engine/model/item~Item|module:engine/model/selection~Selection|module:engine/model/range~Range} item
* Model item, range or selection to be tested.
* @param {String} type Consumable type.
* @returns {null|Boolean} `null` if such consumable was never added, `false` if the consumable values was
* already consumed or `true` if it was added and not consumed yet.
*/
test( item, type ) {
if ( item instanceof textproxy_TextProxy ) {
item = this._getSymbolForTextProxy( item );
}
const itemConsumables = this._consumable.get( item );
if ( itemConsumables === undefined ) {
return null;
}
const value = itemConsumables.get( type );
if ( value === undefined ) {
return null;
}
return value;
}
/**
* Reverts consuming of consumable value.
*
* modelConsumable.revert( modelElement, 'insert' ); // Revert consuming `modelElement` insertion change.
* modelConsumable.revert( modelElement, 'addAttribute:bold' ); // Revert consuming `bold` attribute insert from `modelElement`.
* modelConsumable.revert( modelElement, 'removeAttribute:bold' ); // Revert consuming `bold` attribute remove from `modelElement`.
* modelConsumable.revert( modelSelection, 'selection' ); // Revert consuming `modelSelection`.
* modelConsumable.revert( modelSelection, 'selectionAttribute:bold' ); // Revert consuming `bold` from `modelSelection`.
* modelConsumable.revert( modelRange, 'range' ); // Revert consuming `modelRange`.
*
* @param {module:engine/model/item~Item|module:engine/model/selection~Selection|module:engine/model/range~Range} item
* Model item, range or selection to be reverted.
* @param {String} type Consumable type.
* @returns {null|Boolean} `true` if consumable has been reversed, `false` otherwise. `null` if the consumable has
* never been added.
*/
revert( item, type ) {
if ( item instanceof textproxy_TextProxy ) {
item = this._getSymbolForTextProxy( item );
}
const test = this.test( item, type );
if ( test === false ) {
this._consumable.get( item ).set( type, true );
return true;
} else if ( test === true ) {
return false;
}
return null;
}
/**
* Gets a unique symbol for passed {@link module:engine/model/textproxy~TextProxy} instance. All `TextProxy` instances that
* have same parent, same start index and same end index will get the same symbol.
*
* Used internally to correctly consume `TextProxy` instances.
*
* @private
* @param {module:engine/model/textproxy~TextProxy} textProxy `TextProxy` instance to get a symbol for.
* @returns {Symbol} Symbol representing all equal instances of `TextProxy`.
*/
_getSymbolForTextProxy( textProxy ) {
let symbol = null;
const startMap = this._textProxyRegistry.get( textProxy.startOffset );
if ( startMap ) {
const endMap = startMap.get( textProxy.endOffset );
if ( endMap ) {
symbol = endMap.get( textProxy.parent );
}
}
if ( !symbol ) {
symbol = this._addSymbolForTextProxy( textProxy.startOffset, textProxy.endOffset, textProxy.parent );
}
return symbol;
}
/**
* Adds a symbol for given properties that characterizes a {@link module:engine/model/textproxy~TextProxy} instance.
*
* Used internally to correctly consume `TextProxy` instances.
*
* @private
* @param {Number} startIndex Text proxy start index in it's parent.
* @param {Number} endIndex Text proxy end index in it's parent.
* @param {module:engine/model/element~Element} parent Text proxy parent.
* @returns {Symbol} Symbol generated for given properties.
*/
_addSymbolForTextProxy( start, end, parent ) {
const symbol = Symbol( 'textProxySymbol' );
let startMap, endMap;
startMap = this._textProxyRegistry.get( start );
if ( !startMap ) {
startMap = new Map();
this._textProxyRegistry.set( start, startMap );
}
endMap = startMap.get( end );
if ( !endMap ) {
endMap = new Map();
startMap.set( end, endMap );
}
endMap.set( parent, symbol );
return symbol;
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/documentfragment.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module module:engine/model/documentfragment
*/
/**
* DocumentFragment represents a part of model which does not have a common root but it's top-level nodes
* can be seen as siblings. In other words, it is a detached part of model tree, without a root.
*
* DocumentFragment has own {@link module:engine/model/markercollection~MarkerCollection}. Markers from this collection
* will be set to the {@link module:engine/model/document~Document#markers document markers} by a
* {@link module:engine/model/writer~writer.insert} function.
*/
class documentfragment_DocumentFragment {
/**
* Creates an empty `DocumentFragment`.
*
* @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} [children]
* Nodes to be contained inside the `DocumentFragment`.
*/
constructor( children ) {
/**
* DocumentFragment static markers map. This is a list of names and {@link module:engine/model/range~Range ranges}
* which will be set as Markers to {@link module:engine/model/document~Document#markers document markers collection}
* when DocumentFragment will be inserted to the document.
*
* @member {Map<String, {module:engine/model/range~Range}>} module:engine/model/documentfragment~DocumentFragment#markers
*/
this.markers = new Map();
/**
* List of nodes contained inside the document fragment.
*
* @private
* @member {module:engine/model/nodelist~NodeList} module:engine/model/documentfragment~DocumentFragment#_children
*/
this._children = new nodelist_NodeList();
if ( children ) {
this.insertChildren( 0, children );
}
}
/**
* Returns an iterator that iterates over all nodes contained inside this document fragment.
*
* @returns {Iterator.<module:engine/model/node~Node>}
*/
[ Symbol.iterator ]() {
return this.getChildren();
}
/**
* Number of this document fragment's children.
*
* @readonly
* @type {Number}
*/
get childCount() {
return this._children.length;
}
/**
* Sum of {module:engine/model/node~Node#offsetSize offset sizes} of all of this document fragment's children.
*
* @readonly
* @type {Number}
*/
get maxOffset() {
return this._children.maxOffset;
}
/**
* Is `true` if there are no nodes inside this document fragment, `false` otherwise.
*
* @readonly
* @type {Boolean}
*/
get isEmpty() {
return this.childCount === 0;
}
/**
* Artificial root of `DocumentFragment`. Returns itself. Added for compatibility reasons.
*
* @readonly
* @type {module:engine/model/documentfragment~DocumentFragment}
*/
get root() {
return this;
}
/**
* Artificial parent of `DocumentFragment`. Returns `null`. Added for compatibility reasons.
*
* @readonly
* @type {null}
*/
get parent() {
return null;
}
/**
* Checks whether given model tree object is of given type.
*
* Read more in {@link module:engine/model/node~Node#is}.
*
* @param {String} type
* @returns {Boolean}
*/
is( type ) {
return type == 'documentFragment';
}
/**
* Gets the child at the given index. Returns `null` if incorrect index was passed.
*
* @param {Number} index Index of child.
* @returns {module:engine/model/node~Node|null} Child node.
*/
getChild( index ) {
return this._children.getNode( index );
}
/**
* Returns an iterator that iterates over all of this document fragment's children.
*
* @returns {Iterable.<module:engine/model/node~Node>}
*/
getChildren() {
return this._children[ Symbol.iterator ]();
}
/**
* Returns an index of the given child node. Returns `null` if given node is not a child of this document fragment.
*
* @param {module:engine/model/node~Node} node Child node to look for.
* @returns {Number|null} Child node's index.
*/
getChildIndex( node ) {
return this._children.getNodeIndex( node );
}
/**
* Returns the starting offset of given child. Starting offset is equal to the sum of
* {module:engine/model/node~Node#offsetSize offset sizes} of all node's siblings that are before it. Returns `null` if
* given node is not a child of this document fragment.
*
* @param {module:engine/model/node~Node} node Child node to look for.
* @returns {Number|null} Child node's starting offset.
*/
getChildStartOffset( node ) {
return this._children.getNodeStartOffset( node );
}
/**
* Returns path to a `DocumentFragment`, which is an empty array. Added for compatibility reasons.
*
* @returns {Array}
*/
getPath() {
return [];
}
/**
* Returns a descendant node by its path relative to this element.
*
* // <this>a<b>c</b></this>
* this.getNodeByPath( [ 0 ] ); // -> "a"
* this.getNodeByPath( [ 1 ] ); // -> <b>
* this.getNodeByPath( [ 1, 0 ] ); // -> "c"
*
* @param {Array.<Number>} relativePath Path of the node to find, relative to this element.
* @returns {module:engine/model/node~Node|module:engine/model/documentfragment~DocumentFragment}
*/
getNodeByPath( relativePath ) {
let node = this; // eslint-disable-line consistent-this
for ( const index of relativePath ) {
node = node.getChild( node.offsetToIndex( index ) );
}
return node;
}
/**
* Converts offset "position" to index "position".
*
* Returns index of a node that occupies given offset. If given offset is too low, returns `0`. If given offset is
* too high, returns index after last child}.
*
* const textNode = new Text( 'foo' );
* const pElement = new Element( 'p' );
* const docFrag = new DocumentFragment( [ textNode, pElement ] );
* docFrag.offsetToIndex( -1 ); // Returns 0, because offset is too low.
* docFrag.offsetToIndex( 0 ); // Returns 0, because offset 0 is taken by `textNode` which is at index 0.
* docFrag.offsetToIndex( 1 ); // Returns 0, because `textNode` has `offsetSize` equal to 3, so it occupies offset 1 too.
* docFrag.offsetToIndex( 2 ); // Returns 0.
* docFrag.offsetToIndex( 3 ); // Returns 1.
* docFrag.offsetToIndex( 4 ); // Returns 2. There are no nodes at offset 4, so last available index is returned.
*
* @param {Number} offset Offset to look for.
* @returns {Number} Index of a node that occupies given offset.
*/
offsetToIndex( offset ) {
return this._children.offsetToIndex( offset );
}
/**
* {@link #insertChildren Inserts} one or more nodes at the end of this document fragment.
*
* @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted.
*/
appendChildren( nodes ) {
this.insertChildren( this.childCount, nodes );
}
/**
* Inserts one or more nodes at the given index and sets {@link module:engine/model/node~Node#parent parent} of these nodes
* to this document fragment.
*
* @param {Number} index Index at which nodes should be inserted.
* @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted.
*/
insertChildren( index, nodes ) {
nodes = documentfragment_normalize( nodes );
for ( const node of nodes ) {
// If node that is being added to this element is already inside another element, first remove it from the old parent.
if ( node.parent !== null ) {
node.remove();
}
node.parent = this;
}
this._children.insertNodes( index, nodes );
}
/**
* Removes one or more nodes starting at the given index
* and sets {@link module:engine/model/node~Node#parent parent} of these nodes to `null`.
*
* @param {Number} index Index of the first node to remove.
* @param {Number} [howMany=1] Number of nodes to remove.
* @returns {Array.<module:engine/model/node~Node>} Array containing removed nodes.
*/
removeChildren( index, howMany = 1 ) {
const nodes = this._children.removeNodes( index, howMany );
for ( const node of nodes ) {
node.parent = null;
}
return nodes;
}
/**
* Converts `DocumentFragment` instance to plain object and returns it.
* Takes care of converting all of this document fragment's children.
*
* @returns {Object} `DocumentFragment` instance converted to plain object.
*/
toJSON() {
const json = [];
for ( const node of this._children ) {
json.push( node.toJSON() );
}
return json;
}
/**
* Creates a `DocumentFragment` instance from given plain object (i.e. parsed JSON string).
* Converts `DocumentFragment` children to proper nodes.
*
* @param {Object} json Plain object to be converted to `DocumentFragment`.
* @returns {module:engine/model/documentfragment~DocumentFragment} `DocumentFragment` instance created using given plain object.
*/
static fromJSON( json ) {
const children = [];
for ( const child of json ) {
if ( child.name ) {
// If child has name property, it is an Element.
children.push( element_Element.fromJSON( child ) );
} else {
// Otherwise, it is a Text node.
children.push( text_Text.fromJSON( child ) );
}
}
return new documentfragment_DocumentFragment( children );
}
}
// Converts strings to Text and non-iterables to arrays.
//
// @param {String|module:engine/model/node~Node|Iterable.<String|module:engine/model/node~Node>}
// @return {Iterable.<module:engine/model/node~Node>}
function documentfragment_normalize( nodes ) {
// Separate condition because string is iterable.
if ( typeof nodes == 'string' ) {
return [ new text_Text( nodes ) ];
}
if ( !isIterable( nodes ) ) {
nodes = [ nodes ];
}
// Array.from to enable .map() on non-arrays.
return Array.from( nodes )
.map( node => {
return typeof node == 'string' ? new text_Text( node ) : node;
} );
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/conversion/modelconversiondispatcher.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/conversion/modelconversiondispatcher
*/
/**
* `ModelConversionDispatcher` is a central point of {@link module:engine/model/model model} conversion, which is
* a process of reacting to changes in the model and reflecting them by listeners that listen to those changes.
* In default application, {@link module:engine/model/model model} is converted to {@link module:engine/view/view view}. This means
* that changes in the model are reflected by changing the view (i.e. adding view nodes or changing attributes on view elements).
*
* During conversion process, `ModelConversionDispatcher` fires data-manipulation events, basing on state of the model and prepares
* data for those events. It is important to note that the events are connected with "change actions" like "inserting"
* or "removing" so one might say that we are converting "changes". This is in contrary to view to model conversion,
* where we convert view nodes (the structure, not "changes" to the view). Note, that because changes are converted
* and not the structure itself, there is a need to have a mapping between model and the structure on which changes are
* reflected. To map elements during model to view conversion use {@link module:engine/conversion/mapper~Mapper}.
*
* The main use for this class is to listen to {@link module:engine/model/document~Document#event:change Document change event}, process it
* and then fire specific events telling what exactly has changed. For those events, `ModelConversionDispatcher`
* creates {@link module:engine/conversion/modelconsumable~ModelConsumable list of consumable values} that should be handled by event
* callbacks. Those events are listened to by model-to-view converters which convert changes done in the
* {@link module:engine/model/model model} to changes in the {@link module:engine/view/view view}. `ModelConversionController` also checks
* the current state of consumables, so it won't fire events for parts of model that were already consumed. This is
* especially important in callbacks that consume multiple values. See {@link module:engine/conversion/modelconsumable~ModelConsumable}
* for an example of such callback.
*
* Although the primary usage for this class is the model-to-view conversion, `ModelConversionDispatcher` can be used
* to build custom data processing pipelines that converts model to anything that is needed. Existing model structure can
* be used to generate events (listening to {@link module:engine/model/document~Document#event:change Document change event} is not
* required)
* and custom callbacks can be added to the events (these does not have to be limited to changes in the view).
*
* When providing your own event listeners for `ModelConversionDispatcher` keep in mind that any callback that had
* {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} a value from consumable (and did some changes, i.e. to
* the view) should also stop the event. This is because whenever a callback is fired it is assumed that there is something
* to be consumed. Thanks to that approach, you do not have to test whether there is anything to consume at the beginning
* of your listener callback.
*
* Example of providing a converter for `ModelConversionDispatcher`:
*
* // We will convert inserting "paragraph" model element into the model.
* modelDispatcher.on( 'insert:paragraph', ( evt, data, consumable, conversionApi ) => {
* // Remember to consume the part of consumable.
* consumable.consume( data.item, 'insert' );
*
* // Translate position in model to position in the view.
* const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
*
* // Create a P element (note that this converter is for inserting P elements -> 'insert:paragraph').
* const viewElement = new ViewElement( 'p' );
*
* // Bind the newly created view element to model element so positions will map accordingly in future.
* conversionApi.mapper.bindElements( data.item, viewElement );
*
* // Add the newly created view element to the view.
* viewWriter.insert( viewPosition, viewElement );
*
* // Remember to stop the event propagation if the data.item was consumed.
* evt.stop();
* } );
*
* Callback that "overrides" other callback:
*
* // Special converter for `linkHref` attribute added on custom `quote` element. Note, that this
* // attribute may be the same as the attribute added by other features (link feature in this case).
* // It might be even added by that feature! It makes sense that a part of content that is a quote is linked
* // to an external source so it makes sense that link feature works on the custom quote element.
* // However, we have to make sure that the attributes added by link feature are correctly converted.
* // To block default `linkHref` conversion we have to:
* // 1) add this callback with higher priority than link feature callback,
* // 2) consume `linkHref` attribute add change.
* modelConversionDispatcher.on( 'addAttribute:linkHref:quote', ( evt, data, consumable, conversionApi ) => {
* consumable.consume( data.item, 'addAttribute:linkHref' );
*
* // Create a button that will represent the `linkHref` attribute.
* let viewSourceBtn = new ViewElement( 'a', {
* href: data.item.getAttribute( 'linkHref' ),
* title: 'source'
* } );
*
* // Add a class for the button.
* viewSourceBtn.addClass( 'source' );
*
* // Insert the button using writer API.
* // If `addAttribute` event is fired by
* // `module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#convertInsert` it is fired
* // after `data.item` insert conversion was done. If the event is fired due to attribute insertion coming from
* // different source, `data.item` already existed. This means we are safe to get `viewQuote` from mapper.
* const viewQuote = conversionApi.mapper.toViewElement( data.item );
* const position = new ViewPosition( viewQuote, viewQuote.childCount );
* viewWriter.insert( position, viewSourceBtn );
*
* evt.stop();
* }, { priority: 'high' } );
*/
class modelconversiondispatcher_ModelConversionDispatcher {
/**
* Creates a `ModelConversionDispatcher` that operates using passed API.
*
* @param {module:engine/model/document~Document} modelDocument Model document instance bound with this dispatcher.
* @param {Object} [conversionApi] Interface passed by dispatcher to the events callbacks.
*/
constructor( modelDocument, conversionApi = {} ) {
/**
* Model document instance bound with this dispatcher.
*
* @private
* @member {module:engine/model/document~Document}
*/
this._modelDocument = modelDocument;
/**
* Interface passed by dispatcher to the events callbacks.
*
* @member {Object}
*/
this.conversionApi = lodash_assignIn( { dispatcher: this }, conversionApi );
}
/**
* Prepares data and fires a proper event.
*
* The method is crafted to take use of parameters passed in {@link module:engine/model/document~Document#event:change Document change
* event}.
*
* @see module:engine/model/document~Document#event:change
* @fires insert
* @fires remove
* @fires addAttribute
* @fires removeAttribute
* @fires changeAttribute
* @fires addMarker
* @param {String} type Change type.
* @param {Object} data Additional information about the change.
*/
convertChange( type, data ) {
// Do not convert changes if they happen in graveyard.
// Graveyard is a special root that has no view / no other representation and changes done in it should not be converted.
if ( type !== 'remove' && data.range && data.range.root.rootName == '$graveyard' ) {
return;
}
if ( type == 'remove' && data.sourcePosition.root.rootName == '$graveyard' ) {
return;
}
if ( type == 'rename' && data.element.root.rootName == '$graveyard' ) {
return;
}
// We can safely dispatch changes.
if ( type == 'insert' || type == 'reinsert' ) {
this.convertInsertion( data.range );
} else if ( type == 'move' ) {
this.convertMove( data.sourcePosition, data.range );
} else if ( type == 'remove' ) {
this.convertRemove( data.sourcePosition, data.range );
} else if ( type == 'addAttribute' || type == 'removeAttribute' || type == 'changeAttribute' ) {
this.convertAttribute( type, data.range, data.key, data.oldValue, data.newValue );
} else if ( type == 'rename' ) {
this.convertRename( data.element, data.oldName );
}
}
/**
* Starts conversion of insertion-change on given `range`.
*
* Analyzes given range and fires insertion-connected events with data based on that range.
*
* **Note**: This method will fire separate events for node insertion and attributes insertion. All
* attributes that are set on inserted nodes are treated like they were added just after node insertion.
*
* @fires insert
* @fires addAttribute
* @fires addMarker
* @param {module:engine/model/range~Range} range Inserted range.
*/
convertInsertion( range ) {
// Create a list of things that can be consumed, consisting of nodes and their attributes.
const consumable = this._createInsertConsumable( range );
// Fire a separate insert event for each node and text fragment contained in the range.
for ( const value of range ) {
const item = value.item;
const itemRange = range_Range.createFromPositionAndShift( value.previousPosition, value.length );
const data = {
item,
range: itemRange
};
this._testAndFire( 'insert', data, consumable );
// Fire a separate addAttribute event for each attribute that was set on inserted items.
// This is important because most attributes converters will listen only to add/change/removeAttribute events.
// If we would not add this part, attributes on inserted nodes would not be converted.
for ( const key of item.getAttributeKeys() ) {
data.attributeKey = key;
data.attributeOldValue = null;
data.attributeNewValue = item.getAttribute( key );
this._testAndFire( `addAttribute:${ key }`, data, consumable );
}
}
for ( const marker of this._modelDocument.markers ) {
const markerRange = marker.getRange();
const intersection = markerRange.getIntersection( range );
// Check if inserted content is inserted into a marker.
if ( intersection && shouldMarkerChangeBeConverted( range.start, marker, this.conversionApi.mapper ) ) {
this.convertMarker( 'addMarker', marker.name, intersection );
}
}
}
/**
* Starts conversion of move-change of given `range`, that was moved from given `sourcePosition`.
*
* Fires {@link ~#event:remove remove event} and {@link ~#event:insert insert event} based on passed parameters.
*
* @fires remove
* @fires insert
* @param {module:engine/model/position~Position} sourcePosition The original position from which the range was moved.
* @param {module:engine/model/range~Range} range The range containing the moved content.
*/
convertMove( sourcePosition, range ) {
// Move left – convert insertion first (#847).
if ( range.start.isBefore( sourcePosition ) ) {
this.convertInsertion( range );
const sourcePositionAfterInsertion =
sourcePosition._getTransformedByInsertion( range.start, range.end.offset - range.start.offset );
this.convertRemove( sourcePositionAfterInsertion, range );
} else {
this.convertRemove( sourcePosition, range );
this.convertInsertion( range );
}
}
/**
* Starts conversion of remove-change of given `range`, that was removed from given `sourcePosition`.
*
* Fires {@link ~#event:remove remove event} with data based on passed values.
*
* @fires remove
* @param {module:engine/model/position~Position} sourcePosition Position from where the range has been removed.
* @param {module:engine/model/range~Range} range Removed range (after remove, in
* {@link module:engine/model/document~Document#graveyard graveyard root}).
*/
convertRemove( sourcePosition, range ) {
const consumable = this._createConsumableForRange( range, 'remove' );
for ( const item of range.getItems( { shallow: true } ) ) {
const data = {
sourcePosition,
item
};
this._testAndFire( 'remove', data, consumable );
}
}
/**
* Starts conversion of attribute-change on given `range`.
*
* Analyzes given attribute change and fires attributes-connected events with data based on passed values.
*
* @fires addAttribute
* @fires removeAttribute
* @fires changeAttribute
* @param {String} type Change type. Possible values: `addAttribute`, `removeAttribute`, `changeAttribute`.
* @param {module:engine/model/range~Range} range Changed range.
* @param {String} key Attribute key.
* @param {*} oldValue Attribute value before the change or `null` if attribute has not been set.
* @param {*} newValue New attribute value or `null` if attribute has been removed.
*/
convertAttribute( type, range, key, oldValue, newValue ) {
if ( oldValue == newValue ) {
// Do not convert if the attribute did not change.
return;
}
// Create a list with attributes to consume.
const consumable = this._createConsumableForRange( range, type + ':' + key );
// Create a separate attribute event for each node in the range.
for ( const value of range ) {
const item = value.item;
const itemRange = range_Range.createFromPositionAndShift( value.previousPosition, value.length );
const data = {
item,
range: itemRange,
attributeKey: key,
attributeOldValue: oldValue,
attributeNewValue: newValue
};
this._testAndFire( `${ type }:${ key }`, data, consumable );
}
}
/**
* Starts conversion of rename-change of given `element` that had given `oldName`.
*
* Fires {@link ~#event:remove remove event} and {@link ~#event:insert insert event} based on passed values.
*
* @fires remove
* @fires insert
* @param {module:engine/model/element~Element} element Renamed element.
* @param {String} oldName Name of the renamed element before it was renamed.
*/
convertRename( element, oldName ) {
if ( element.name == oldName ) {
// Do not convert if the name did not change.
return;
}
// Create fake element that will be used to fire remove event. The fake element will have the old element name.
const fakeElement = element.clone( true );
fakeElement.name = oldName;
// Bind fake element with original view element so the view element will be removed.
this.conversionApi.mapper.bindElements(
fakeElement,
this.conversionApi.mapper.toViewElement( element )
);
// Create fake document fragment so a range can be created on fake element.
const fakeDocumentFragment = new documentfragment_DocumentFragment();
fakeDocumentFragment.appendChildren( fakeElement );
this.convertRemove( position_Position.createBefore( element ), range_Range.createOn( fakeElement ) );
this.convertInsertion( range_Range.createOn( element ) );
}
/**
* Starts selection conversion.
*
* Fires events for given {@link module:engine/model/selection~Selection selection} to start selection conversion.
*
* @fires selection
* @fires selectionAttribute
* @param {module:engine/model/selection~Selection} selection Selection to convert.
*/
convertSelection( selection ) {
const markers = Array.from( this._modelDocument.markers.getMarkersAtPosition( selection.getFirstPosition() ) );
const consumable = this._createSelectionConsumable( selection, markers );
this.fire( 'selection', { selection }, consumable, this.conversionApi );
for ( const marker of markers ) {
const markerRange = marker.getRange();
if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) {
continue;
}
const data = {
selection,
markerName: marker.name,
markerRange
};
if ( consumable.test( selection, 'selectionMarker:' + marker.name ) ) {
this.fire( 'selectionMarker:' + marker.name, data, consumable, this.conversionApi );
}
}
for ( const key of selection.getAttributeKeys() ) {
const data = {
selection,
key,
value: selection.getAttribute( key )
};
// Do not fire event if the attribute has been consumed.
if ( consumable.test( selection, 'selectionAttribute:' + data.key ) ) {
this.fire( 'selectionAttribute:' + data.key, data, consumable, this.conversionApi );
}
}
}
/**
* Starts marker conversion.
*
* Fires {@link ~#event:addMarker addMarker} or {@link ~#event:removeMarker removeMarker} events for each item
* in marker's range. If range is collapsed single event is dispatched. See events description for more details.
*
* @fires addMarker
* @fires removeMarker
* @param {'addMarker'|'removeMarker'} type Change type.
* @param {String} name Marker name.
* @param {module:engine/model/range~Range} range Marker range.
*/
convertMarker( type, name, range ) {
// Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment).
if ( !range.root.document || range.root.rootName == '$graveyard' ) {
return;
}
// In markers case, event name == consumable name.
const eventName = type + ':' + name;
// When range is collapsed - fire single event with collapsed range in consumable.
if ( range.isCollapsed ) {
const consumable = new modelconsumable_ModelConsumable();
consumable.add( range, eventName );
this.fire( eventName, {
markerName: name,
markerRange: range
}, consumable, this.conversionApi );
return;
}
// Create consumable for each item in range.
const consumable = this._createConsumableForRange( range, eventName );
// Create separate event for each node in the range.
for ( const value of range ) {
const item = value.item;
// Do not fire event for already consumed items.
if ( !consumable.test( item, eventName ) ) {
continue;
}
const data = {
item,
range: range_Range.createFromPositionAndShift( value.previousPosition, value.length ),
markerName: name,
markerRange: range
};
this.fire( eventName, data, consumable, this.conversionApi );
}
}
/**
* Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume from given range, assuming that
* given range has just been inserted to the model.
*
* @private
* @param {module:engine/model/range~Range} range Inserted range.
* @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
*/
_createInsertConsumable( range ) {
const consumable = new modelconsumable_ModelConsumable();
for ( const value of range ) {
const item = value.item;
consumable.add( item, 'insert' );
for ( const key of item.getAttributeKeys() ) {
consumable.add( item, 'addAttribute:' + key );
}
}
return consumable;
}
/**
* Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values of given `type`
* for each item from given `range`.
*
* @private
* @param {module:engine/model/range~Range} range Affected range.
* @param {String} type Consumable type.
* @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
*/
_createConsumableForRange( range, type ) {
const consumable = new modelconsumable_ModelConsumable();
for ( const item of range.getItems() ) {
consumable.add( item, type );
}
return consumable;
}
/**
* Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with selection consumable values.
*
* @private
* @param {module:engine/model/selection~Selection} selection Selection to create consumable from.
* @param {Iterable.<module:engine/model/markercollection~Marker>} markers Markers which contains selection.
* @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
*/
_createSelectionConsumable( selection, markers ) {
const consumable = new modelconsumable_ModelConsumable();
consumable.add( selection, 'selection' );
for ( const marker of markers ) {
consumable.add( selection, 'selectionMarker:' + marker.name );
}
for ( const key of selection.getAttributeKeys() ) {
consumable.add( selection, 'selectionAttribute:' + key );
}
return consumable;
}
/**
* Tests passed `consumable` to check whether given event can be fired and if so, fires it.
*
* @private
* @fires insert
* @fires remove
* @fires addAttribute
* @fires removeAttribute
* @fires changeAttribute
* @param {String} type Event type.
* @param {Object} data Event data.
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
*/
_testAndFire( type, data, consumable ) {
if ( !consumable.test( data.item, type ) ) {
// Do not fire event if the item was consumed.
return;
}
const name = data.item.name || '$text';
this.fire( type + ':' + name, data, consumable, this.conversionApi );
}
/**
* Fired for inserted nodes.
*
* `insert` is a namespace for a class of events. Names of actually called events follow this pattern:
* `insert:<name>`. `name` is either `'$text'` when one or more characters has been inserted or
* {@link module:engine/model/element~Element#name name} of inserted element.
*
* This way listeners can either listen to a general `insert` event or specific event (for example `insert:paragraph`).
*
* @event insert
* @param {Object} data Additional information about the change.
* @param {module:engine/model/item~Item} data.item Inserted item.
* @param {module:engine/model/range~Range} data.range Range spanning over inserted item.
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
*/
/**
* Fired for removed nodes.
*
* `remove` is a namespace for a class of events. Names of actually called events follow this pattern:
* `remove:<name>`. `name` is either `'$text'` when one or more characters has been removed or the
* {@link module:engine/model/element~Element#name name} of removed element.
*
* This way listeners can either listen to a general `remove` event or specific event (for example `remove:paragraph`).
*
* @event remove
* @param {Object} data Additional information about the change.
* @param {module:engine/model/position~Position} data.sourcePosition Position from where the range has been removed.
* @param {module:engine/model/range~Range} data.range Removed range (in {@link module:engine/model/document~Document#graveyard
* graveyard root}).
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
*/
/**
* Fired when attribute has been added on a node.
*
* `addAttribute` is a namespace for a class of events. Names of actually called events follow this pattern:
* `addAttribute:<attributeKey>:<name>`. `attributeKey` is the key of added attribute. `name` is either `'$text'`
* if attribute was added on one or more characters, or the {@link module:engine/model/element~Element#name name} of
* the element on which attribute was added.
*
* This way listeners can either listen to a general `addAttribute:bold` event or specific event
* (for example `addAttribute:link:image`).
*
* @event addAttribute
* @param {Object} data Additional information about the change.
* @param {module:engine/model/item~Item} data.item Changed item.
* @param {module:engine/model/range~Range} data.range Range spanning over changed item.
* @param {String} data.attributeKey Attribute key.
* @param {null} data.attributeOldValue Attribute value before the change - always `null`. Kept for the sake of unifying events.
* @param {*} data.attributeNewValue New attribute value.
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
*/
/**
* Fired when attribute has been removed from a node.
*
* `removeAttribute` is a namespace for a class of events. Names of actually called events follow this pattern:
* `removeAttribute:<attributeKey>:<name>`. `attributeKey` is the key of removed attribute. `name` is either `'$text'`
* if attribute was removed from one or more characters, or the {@link module:engine/model/element~Element#name name} of
* the element from which attribute was removed.
*
* This way listeners can either listen to a general `removeAttribute:bold` event or specific event
* (for example `removeAttribute:link:image`).
*
* @event removeAttribute
* @param {Object} data Additional information about the change.
* @param {module:engine/model/item~Item} data.item Changed item.
* @param {module:engine/model/range~Range} data.range Range spanning over changed item.
* @param {String} data.attributeKey Attribute key.
* @param {*} data.attributeOldValue Attribute value before it was removed.
* @param {null} data.attributeNewValue New attribute value - always `null`. Kept for the sake of unifying events.
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
*/
/**
* Fired when attribute of a node has been changed.
*
* `changeAttribute` is a namespace for a class of events. Names of actually called events follow this pattern:
* `changeAttribute:<attributeKey>:<name>`. `attributeKey` is the key of changed attribute. `name` is either `'$text'`
* if attribute was changed on one or more characters, or the {@link module:engine/model/element~Element#name name} of
* the element on which attribute was changed.
*
* This way listeners can either listen to a general `changeAttribute:link` event or specific event
* (for example `changeAttribute:link:image`).
*
* @event changeAttribute
* @param {Object} data Additional information about the change.
* @param {module:engine/model/item~Item} data.item Changed item.
* @param {module:engine/model/range~Range} data.range Range spanning over changed item.
* @param {String} data.attributeKey Attribute key.
* @param {*} data.attributeOldValue Attribute value before the change.
* @param {*} data.attributeNewValue New attribute value.
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
*/
/**
* Fired for {@link module:engine/model/selection~Selection selection} changes.
*
* @event selection
* @param {module:engine/model/selection~Selection} selection `Selection` instance that is converted.
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
*/
/**
* Fired for {@link module:engine/model/selection~Selection selection} attributes changes.
*
* `selectionAttribute` is a namespace for a class of events. Names of actually called events follow this pattern:
* `selectionAttribute:<attributeKey>`. `attributeKey` is the key of selection attribute. This way listen can listen to
* certain attribute, i.e. `addAttribute:bold`.
*
* @event selectionAttribute
* @param {Object} data Additional information about the change.
* @param {module:engine/model/selection~Selection} data.selection Selection that is converted.
* @param {String} data.attributeKey Key of changed attribute.
* @param {*} data.attributeValue Value of changed attribute.
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
*/
/**
* Fired when a new marker is added to the model.
* * If marker's range is not collapsed, event is fired separately for each item contained in that range. In this
* situation, consumable contains all items from that range.
* * If marker's range is collapsed, single event is fired. In this situation, consumable contains only the collapsed range.
*
* `addMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
* `addMarker:<markerName>`. By specifying certain marker names, you can make the events even more gradual. For example,
* markers can be named `foo:abc`, `foo:bar`, then it is possible to listen to `addMarker:foo` or `addMarker:foo:abc` and
* `addMarker:foo:bar` events.
*
* @event addMarker
* @param {Object} data Additional information about the change.
* @param {module:engine/model/item~Item} [data.item] Item contained in marker's range. Not present if collapsed range
* is being converted.
* @param {module:engine/model/range~Range} [data.range] Range spanning over item. Not present if collapsed range
* is being converted.
* @param {String} data.markerName Name of the marker.
* @param {module:engine/model/range~Range} data.markerRange Marker's range spanning on all items.
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume. When non-collapsed
* marker is being converted then consumable contains all items in marker's range. For collapsed markers it contains
* only marker's range to consume.
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
*/
/**
* Fired when marker is removed from the model.
* * If marker's range is not collapsed, event is fired separately for each item contained in that range. In this
* situation, consumable contains all items from that range.
* * If marker's range is collapsed, single event is fired. In this situation, consumable contains only the collapsed range.
*
* `removeMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
* `removeMarker:<markerName>`. By specifying certain marker names, you can make the events even more gradual. For example,
* markers can be named `foo:abc`, `foo:bar`, then it is possible to listen to `removeMarker:foo` or `removeMarker:foo:abc` and
* `removeMarker:foo:bar` events.
*
* @event removeMarker
* @param {Object} data Additional information about the change.
* @param {module:engine/model/item~Item} [data.item] Item contained in marker's range. Not present if collapsed range
* is being converted.
* @param {module:engine/model/range~Range} [data.range] Range spanning over item. Not present if collapsed range
* is being converted.
* @param {String} data.markerName Name of the marker.
* @param {module:engine/model/range~Range} data.markerRange Whole marker's range.
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume. When non-collapsed
* marker is being converted then consumable contains all items in marker's range. For collapsed markers it contains
* only marker's range to consume.
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
*/
}
mix( modelconversiondispatcher_ModelConversionDispatcher, emittermixin );
// Helper function, checks whether change of `marker` at `modelPosition` should be converted. Marker changes are not
// converted if they happen inside an element with custom conversion method.
//
// @param {module:engine/model/position~Position} modelPosition
// @param {module:engine/model/markercollection~Marker} marker
// @param {module:engine/conversion/mapper~Mapper} mapper
// @returns {Boolean}
function shouldMarkerChangeBeConverted( modelPosition, marker, mapper ) {
const range = marker.getRange();
const ancestors = Array.from( modelPosition.getAncestors() );
ancestors.shift(); // Remove root element. It cannot be passed to `model.Range#containsItem`.
ancestors.reverse();
const hasCustomHandling = ancestors.some( element => {
if ( range.containsItem( element ) ) {
const viewElement = mapper.toViewElement( element );
return !!viewElement.getCustomProperty( 'addHighlight' );
}
} );
return !hasCustomHandling;
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/attributeelement.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/view/attributeelement
*/
// Default attribute priority.
const DEFAULT_PRIORITY = 10;
/**
* Attributes are elements which define document presentation. They are mostly elements like `<b>` or `<span>`.
* Attributes can be broken and merged by the {@link module:engine/view/writer~writer view writer}.
*
* Editing engine does not define fixed HTML DTD. This is why the type of the {@link module:engine/view/element~Element} need to
* be defined by the feature developer. Creating an element you should use {@link module:engine/view/containerelement~ContainerElement}
* class or `AttributeElement`.
*
* @extends module:engine/view/element~Element
*/
class AttributeElement extends view_element_Element {
/**
* Creates a attribute element.
*
* @see module:engine/view/element~Element
*/
constructor( name, attrs, children ) {
super( name, attrs, children );
/**
* Element priority. Attributes have to have the same priority to be
* {@link module:engine/view/element~Element#isSimilar similar}. Setting different priorities on similar
* nodes may prevent merging, e.g. two `<abbr>` nodes next each other shouldn't be merged.
*
* @member {Number}
*/
this.priority = DEFAULT_PRIORITY;
/**
* Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed.
*
* @method #getFillerOffset
* @returns {Number|null} Block filler offset or `null` if block filler is not needed.
*/
this.getFillerOffset = attributeelement_getFillerOffset;
}
/**
* @inheritDoc
*/
is( type, name = null ) {
if ( !name ) {
return type == 'attributeElement' || super.is( type );
} else {
return ( type == 'attributeElement' && name == this.name ) || super.is( type, name );
}
}
/**
* Clones provided element with priority.
*
* @param {Boolean} deep If set to `true` clones element and all its children recursively. When set to `false`,
* element will be cloned without any children.
* @returns {module:engine/view/attributeelement~AttributeElement} Clone of this element.
*/
clone( deep ) {
const cloned = super.clone( deep );
// Clone priority too.
cloned.priority = this.priority;
return cloned;
}
/**
* Checks if this element is similar to other element.
* Both elements should have the same name, attributes and priority to be considered as similar.
* Two similar elements can contain different set of children nodes.
*
* @param {module:engine/view/element~Element} otherElement
* @returns {Boolean}
*/
isSimilar( otherElement ) {
return super.isSimilar( otherElement ) && this.priority == otherElement.priority;
}
}
/**
* Default attribute priority.
*
* @member {Number} module:engine/view/attributeelement~AttributeElement.DEFAULT_PRIORITY
*/
AttributeElement.DEFAULT_PRIORITY = DEFAULT_PRIORITY;
// Returns block {@link module:engine/view/filler~Filler filler} offset or `null` if block filler is not needed.
//
// @returns {Number|null} Block filler offset or `null` if block filler is not needed.
function attributeelement_getFillerOffset() {
// <b>foo</b> does not need filler.
if ( nonUiChildrenCount( this ) ) {
return null;
}
let element = this.parent;
// <p><b></b></p> needs filler -> <p><b><br></b></p>
while ( element && element.is( 'attributeElement' ) ) {
if ( nonUiChildrenCount( element ) > 1 ) {
return null;
}
element = element.parent;
}
if ( !element || nonUiChildrenCount( element ) > 1 ) {
return null;
}
// Render block filler at the end of element (after all ui elements).
return this.childCount;
}
// Returns total count of children that are not {@link module:engine/view/uielement~UIElement UIElements}.
//
// @param {module:engine/view/element~Element} element
// @return {Number}
function nonUiChildrenCount( element ) {
return Array.from( element.getChildren() ).filter( element => !element.is( 'uiElement' ) ).length;
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/emptyelement.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/view/emptyelement
*/
/**
* EmptyElement class. It is used to represent elements that cannot contain any child nodes.
*/
class emptyelement_EmptyElement extends view_element_Element {
/**
* Creates new instance of EmptyElement.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-emptyelement-cannot-add` when third parameter is passed,
* to inform that usage of EmptyElement is incorrect (adding child nodes to EmptyElement is forbidden).
*
* @param {String} name Node name.
* @param {Object|Iterable} [attributes] Collection of attributes.
*/
constructor( name, attributes, children ) {
super( name, attributes, children );
/**
* Returns `null` because filler is not needed for EmptyElements.
*
* @method #getFillerOffset
* @returns {null} Always returns null.
*/
this.getFillerOffset = emptyelement_getFillerOffset;
}
/**
* @inheritDoc
*/
is( type, name = null ) {
if ( !name ) {
return type == 'emptyElement' || super.is( type );
} else {
return ( type == 'emptyElement' && name == this.name ) || super.is( type, name );
}
}
/**
* Overrides {@link module:engine/view/element~Element#insertChildren} method.
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-emptyelement-cannot-add` to prevent
* adding any child nodes to EmptyElement.
*/
insertChildren( index, nodes ) {
if ( nodes && ( nodes instanceof view_node_Node || Array.from( nodes ).length > 0 ) ) {
/**
* Cannot add children to {@link module:engine/view/emptyelement~EmptyElement}.
*
* @error view-emptyelement-cannot-add
*/
throw new CKEditorError( 'view-emptyelement-cannot-add: Cannot add child nodes to EmptyElement instance.' );
}
}
}
// Returns `null` because block filler is not needed for EmptyElements.
//
// @returns {null}
function emptyelement_getFillerOffset() {
return null;
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/env.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/* globals navigator:false */
/**
* @module utils/env
*/
const userAgent = navigator.userAgent.toLowerCase();
/**
* A namespace containing environment and browser information.
*
* @namespace
*/
const env = {
/**
* Indicates that application is running on Macintosh.
*
* @static
* @member {Boolean} module:utils/env~env#mac
*/
mac: isMac( userAgent )
};
/* harmony default export */ var src_env = (env);
/**
* Checks if User Agent represented by the string is running on Macintosh.
*
* @param {String} userAgent **Lowercase** `navigator.userAgent` string.
* @returns {Boolean} Whether User Agent is running on Macintosh or not.
*/
function isMac( userAgent ) {
return userAgent.indexOf( 'macintosh' ) > -1;
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/keyboard.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* Set of utils related to keyboard support.
*
* @module utils/keyboard
*/
/**
* Object with `keyName => keyCode` pairs for a set of known keys.
*
* Contains:
*
* * `a-z`,
* * `0-9`,
* * `f1-f12`,
* * `arrow(left|up|right|bottom)`,
* * `backspace`, `delete`, `enter`, `esc`, `tab`,
* * `ctrl`, `cmd`, `shift`, `alt`.
*/
const keyCodes = generateKnownKeyCodes();
/**
* Converts a key name or a {@link module:utils/keyboard~KeystrokeInfo keystroke info} into a key code.
*
* Note: Key names are matched with {@link module:utils/keyboard~keyCodes} in a case-insensitive way.
*
* @param {String|module:utils/keyboard~KeystrokeInfo} Key name (see {@link module:utils/keyboard~keyCodes})
* or a keystroke data object.
* @returns {Number} Key or keystroke code.
*/
function getCode( key ) {
let keyCode;
if ( typeof key == 'string' ) {
keyCode = keyCodes[ key.toLowerCase() ];
if ( !keyCode ) {
/**
* Unknown key name. Only key names contained by the {@link module:utils/keyboard~keyCodes} can be used.
*
* @errror keyboard-unknown-key
* @param {String} key
*/
throw new CKEditorError( 'keyboard-unknown-key: Unknown key name.', { key } );
}
} else {
keyCode = key.keyCode +
( key.altKey ? keyCodes.alt : 0 ) +
( key.ctrlKey ? keyCodes.ctrl : 0 ) +
( key.shiftKey ? keyCodes.shift : 0 );
}
return keyCode;
}
/**
* Parses keystroke and returns a keystroke code that will match the code returned by
* link {@link module:utils/keyboard.getCode} for a corresponding {@link module:utils/keyboard~KeystrokeInfo keystroke info}.
*
* The keystroke can be passed in two formats:
*
* * as a single string – e.g. `ctrl + A`,
* * as an array of {@link module:utils/keyboard~keyCodes known key names} and key codes – e.g.:
* * `[ 'ctrl', 32 ]` (ctrl + space),
* * `[ 'ctrl', 'a' ]` (ctrl + A).
*
* Note: Key names are matched with {@link module:utils/keyboard~keyCodes} in a case-insensitive way.
*
* Note: Only keystrokes with a single non-modifier key are supported (e.g. `ctrl+A` is OK, but `ctrl+A+B` is not).
*
* @param {String|Array.<Number|String>} keystroke Keystroke definition.
* @returns {Number} Keystroke code.
*/
function parseKeystroke( keystroke ) {
if ( typeof keystroke == 'string' ) {
keystroke = splitKeystrokeText( keystroke );
}
return keystroke
.map( key => ( typeof key == 'string' ) ? getCode( key ) : key )
.reduce( ( key, sum ) => sum + key, 0 );
}
/**
* It translates any keystroke string text like `"CTRL+A"` to an
* environment–specific keystroke, i.e. `"⌘A"` on Mac OSX.
*
* @param {String} keystroke Keystroke text.
* @returns {String} Keystroke text specific for the environment.
*/
function getEnvKeystrokeText( keystroke ) {
const split = splitKeystrokeText( keystroke );
if ( src_env.mac ) {
if ( split[ 0 ].toLowerCase() == 'ctrl' ) {
return '⌘' + ( split[ 1 ] || '' );
}
}
return keystroke;
}
function generateKnownKeyCodes() {
const keyCodes = {
arrowleft: 37,
arrowup: 38,
arrowright: 39,
arrowdown: 40,
backspace: 8,
delete: 46,
enter: 13,
space: 32,
esc: 27,
tab: 9,
// The idea about these numbers is that they do not collide with any real key codes, so we can use them
// like bit masks.
ctrl: 0x110000,
// Has the same code as ctrl, because their behaviour should be unified across the editor.
// See http://ckeditor.github.io/editor-recommendations/general-policies#ctrl-vs-cmd
cmd: 0x110000,
shift: 0x220000,
alt: 0x440000
};
// a-z
for ( let code = 65; code <= 90; code++ ) {
const letter = String.fromCharCode( code );
keyCodes[ letter.toLowerCase() ] = code;
}
// 0-9
for ( let code = 48; code <= 57; code++ ) {
keyCodes[ code - 48 ] = code;
}
// F1-F12
for ( let code = 112; code <= 123; code++ ) {
keyCodes[ 'f' + ( code - 111 ) ] = code;
}
return keyCodes;
}
function splitKeystrokeText( keystroke ) {
return keystroke.split( /\s*\+\s*/ );
}
/**
* Information about a keystroke.
*
* @interface module:utils/keyboard~KeystrokeInfo
*/
/**
* The [key code](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode).
*
* @member {Number} module:utils/keyboard~KeystrokeInfo#keyCode
*/
/**
* Whether the <kbd>Alt</kbd> modifier was pressed.
*
* @member {Bolean} module:utils/keyboard~KeystrokeInfo#altKey
*/
/**
* Whether the <kbd>Ctrl</kbd> or <kbd>Cmd</kbd> modifier was pressed.
*
* @member {Bolean} module:utils/keyboard~KeystrokeInfo#ctrlKey
*/
/**
* Whether the <kbd>Shift</kbd> modifier was pressed.
*
* @member {Bolean} module:utils/keyboard~KeystrokeInfo#shiftKey
*/
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/uielement.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/view/uielement
*/
/**
* UIElement class. It is used to represent UI not a content of the document.
* This element can't be split and selection can't be placed inside this element.
*/
class uielement_UIElement extends view_element_Element {
/**
* Creates new instance of UIElement.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-uielement-cannot-add` when third parameter is passed,
* to inform that usage of UIElement is incorrect (adding child nodes to UIElement is forbidden).
*
* @param {String} name Node name.
* @param {Object|Iterable} [attributes] Collection of attributes.
*/
constructor( name, attributes, children ) {
super( name, attributes, children );
/**
* Returns `null` because filler is not needed for UIElements.
*
* @method #getFillerOffset
* @returns {null} Always returns null.
*/
this.getFillerOffset = uielement_getFillerOffset;
}
/**
* @inheritDoc
*/
is( type, name = null ) {
if ( !name ) {
return type == 'uiElement' || super.is( type );
} else {
return ( type == 'uiElement' && name == this.name ) || super.is( type, name );
}
}
/**
* Overrides {@link module:engine/view/element~Element#insertChildren} method.
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-uielement-cannot-add` to prevent adding any child nodes
* to UIElement.
*/
insertChildren( index, nodes ) {
if ( nodes && ( nodes instanceof view_node_Node || Array.from( nodes ).length > 0 ) ) {
/**
* Cannot add children to {@link module:engine/view/uielement~UIElement}.
*
* @error view-uielement-cannot-add
*/
throw new CKEditorError( 'view-uielement-cannot-add: Cannot add child nodes to UIElement instance.' );
}
}
/**
* Renders this {@link module:engine/view/uielement~UIElement} to DOM. This method is called by
* {@link module:engine/view/domconverter~DomConverter}.
*
* @param {Document} domDocument
* @return {HTMLElement}
*/
render( domDocument ) {
const domElement = domDocument.createElement( this.name );
for ( const key of this.getAttributeKeys() ) {
domElement.setAttribute( key, this.getAttribute( key ) );
}
return domElement;
}
}
/**
* This function injects UI element handling to the given {@link module:engine/view/document~Document document}.
*
* A callback is added to {@link module:engine/view/document~Document#event:keydown document keydown event}.
* The callback handles the situation when right arrow key is pressed and selection is collapsed before a UI element.
* Without this handler, it would be impossible to "jump over" UI element using right arrow key.
*
* @param {module:engine/view/document~Document} document Document to which the quirks handling will be injected.
*/
function injectUiElementHandling( document ) {
document.on( 'keydown', ( evt, data ) => jumpOverUiElement( evt, data, document.domConverter ) );
}
// Returns `null` because block filler is not needed for UIElements.
//
// @returns {null}
function uielement_getFillerOffset() {
return null;
}
// Selection cannot be placed in a `UIElement`. Whenever it is placed there, it is moved before it. This
// causes a situation when it is impossible to jump over `UIElement` using right arrow key, because the selection
// ends up in ui element (in DOM) and is moved back to the left. This handler fixes this situation.
function jumpOverUiElement( evt, data, domConverter ) {
if ( data.keyCode == keyCodes.arrowright ) {
const domSelection = data.domTarget.ownerDocument.defaultView.getSelection();
const domSelectionCollapsed = domSelection.rangeCount == 1 && domSelection.getRangeAt( 0 ).collapsed;
// Jump over UI element if selection is collapsed or shift key is pressed. These are the cases when selection would extend.
if ( domSelectionCollapsed || data.shiftKey ) {
const domParent = domSelection.focusNode;
const domOffset = domSelection.focusOffset;
const viewPosition = domConverter.domPositionToView( domParent, domOffset );
// In case if dom element is not converted to view or is not mapped or something. Happens for example in some tests.
if ( viewPosition === null ) {
return;
}
// Skip all following ui elements.
let jumpedOverAnyUiElement = false;
const nextViewPosition = viewPosition.getLastMatchingPosition( value => {
if ( value.item.is( 'uiElement' ) ) {
// Remember that there was at least one ui element.
jumpedOverAnyUiElement = true;
}
// Jump over ui elements, jump over empty attribute elements, move up from inside of attribute element.
if ( value.item.is( 'uiElement' ) || value.item.is( 'attributeElement' ) ) {
return true;
}
// Don't jump over text or don't get out of container element.
return false;
} );
// If anything has been skipped, fix position.
// This `if` could be possibly omitted but maybe it is better not to mess with DOM selection if not needed.
if ( jumpedOverAnyUiElement ) {
const newDomPosition = domConverter.viewPositionToDom( nextViewPosition );
if ( domSelectionCollapsed ) {
// Selection was collapsed, so collapse it at further position.
domSelection.collapse( newDomPosition.parent, newDomPosition.offset );
} else {
// Selection was not collapse, so extend it instead of collapsing.
domSelection.extend( newDomPosition.parent, newDomPosition.offset );
}
}
}
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/documentfragment.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/view/documentfragment
*/
/**
* DocumentFragment class.
*/
class view_documentfragment_DocumentFragment {
/**
* Creates new DocumentFragment instance.
*
* @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children] List of nodes to be inserted into
* created document fragment.
*/
constructor( children ) {
/**
* Array of child nodes.
*
* @protected
* @member {Array.<module:engine/view/element~Element>} module:engine/view/documentfragment~DocumentFragment#_children
*/
this._children = [];
if ( children ) {
this.insertChildren( 0, children );
}
}
/**
* Iterates over nodes added to this DocumentFragment.
*/
[ Symbol.iterator ]() {
return this._children[ Symbol.iterator ]();
}
/**
* Number of child nodes in this document fragment.
*
* @readonly
* @type {Number}
*/
get childCount() {
return this._children.length;
}
/**
* Is `true` if there are no nodes inside this document fragment, `false` otherwise.
*
* @readonly
* @type {Boolean}
*/
get isEmpty() {
return this.childCount === 0;
}
/**
* Artificial root of `DocumentFragment`. Returns itself. Added for compatibility reasons.
*
* @readonly
* @type {module:engine/model/documentfragment~DocumentFragment}
*/
get root() {
return this;
}
/**
* Artificial parent of `DocumentFragment`. Returns `null`. Added for compatibility reasons.
*
* @readonly
* @type {null}
*/
get parent() {
return null;
}
/**
* Checks whether given view tree object is of given type.
*
* Read more in {@link module:engine/view/node~Node#is}.
*
* @param {String} type
* @returns {Boolean}
*/
is( type ) {
return type == 'documentFragment';
}
/**
* {@link module:engine/view/documentfragment~DocumentFragment#insertChildren Insert} a child node or a list of child nodes at the end
* and sets the parent of these nodes to this fragment.
*
* @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} nodes Node or the list of nodes to be inserted.
* @returns {Number} Number of appended nodes.
*/
appendChildren( nodes ) {
return this.insertChildren( this.childCount, nodes );
}
/**
* Gets child at the given index.
*
* @param {Number} index Index of child.
* @returns {module:engine/view/node~Node} Child node.
*/
getChild( index ) {
return this._children[ index ];
}
/**
* Gets index of the given child node. Returns `-1` if child node is not found.
*
* @param {module:engine/view/node~Node} node Child node.
* @returns {Number} Index of the child node.
*/
getChildIndex( node ) {
return this._children.indexOf( node );
}
/**
* Gets child nodes iterator.
*
* @returns {Iterable.<module:engine/view/node~Node>} Child nodes iterator.
*/
getChildren() {
return this._children[ Symbol.iterator ]();
}
/**
* Inserts a child node or a list of child nodes on the given index and sets the parent of these nodes to
* this fragment.
*
* @param {Number} index Position where nodes should be inserted.
* @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} nodes Node or list of nodes to be inserted.
* @returns {Number} Number of inserted nodes.
*/
insertChildren( index, nodes ) {
this._fireChange( 'children', this );
let count = 0;
nodes = view_documentfragment_normalize( nodes );
for ( const node of nodes ) {
// If node that is being added to this element is already inside another element, first remove it from the old parent.
if ( node.parent !== null ) {
node.remove();
}
node.parent = this;
this._children.splice( index, 0, node );
index++;
count++;
}
return count;
}
/**
* Removes number of child nodes starting at the given index and set the parent of these nodes to `null`.
*
* @param {Number} index Number of the first node to remove.
* @param {Number} [howMany=1] Number of nodes to remove.
* @returns {Array.<module:engine/view/node~Node>} The array of removed nodes.
*/
removeChildren( index, howMany = 1 ) {
this._fireChange( 'children', this );
for ( let i = index; i < index + howMany; i++ ) {
this._children[ i ].parent = null;
}
return this._children.splice( index, howMany );
}
/**
* Fires `change` event with given type of the change.
*
* @private
* @param {module:engine/view/document~ChangeType} type Type of the change.
* @param {module:engine/view/node~Node} node Changed node.
* @fires module:engine/view/node~Node#change
*/
_fireChange( type, node ) {
this.fire( 'change:' + type, node );
}
}
mix( view_documentfragment_DocumentFragment, emittermixin );
// Converts strings to Text and non-iterables to arrays.
//
// @param {String|module:engine/view/node~Node|Iterable.<String|module:engine/view/node~Node>}
// @return {Iterable.<module:engine/view/node~Node>}
function view_documentfragment_normalize( nodes ) {
// Separate condition because string is iterable.
if ( typeof nodes == 'string' ) {
return [ new view_text_Text( nodes ) ];
}
if ( !isIterable( nodes ) ) {
nodes = [ nodes ];
}
// Array.from to enable .map() on non-arrays.
return Array.from( nodes )
.map( node => {
return typeof node == 'string' ? new view_text_Text( node ) : node;
} );
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/writer.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module module:engine/view/writer
*/
/**
* Contains functions used for composing view tree.
*
* @namespace writer
*/
const writer = {
breakAttributes,
breakContainer,
mergeAttributes,
mergeContainers,
insert: writer_insert,
remove: writer_remove,
clear,
move: writer_move,
wrap,
wrapPosition,
unwrap,
rename
};
/* harmony default export */ var view_writer = (writer);
/**
* Breaks attribute nodes at provided position or at boundaries of provided range. It breaks attribute elements inside
* up to a container element.
*
* In following examples `<p>` is a container, `<b>` and `<u>` are attribute nodes:
*
* <p>foo<b><u>bar{}</u></b></p> -> <p>foo<b><u>bar</u></b>[]</p>
* <p>foo<b><u>{}bar</u></b></p> -> <p>foo{}<b><u>bar</u></b></p>
* <p>foo<b><u>b{}ar</u></b></p> -> <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
* <p><b>fo{o</b><u>ba}r</u></p> -> <p><b>fo</b><b>o</b><u>ba</u><u>r</u></b></p>
*
* **Note:** {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} is treated like a container.
*
* **Note:** Difference between {@link module:engine/view/writer~writer.breakAttributes breakAttributes} and
* {@link module:engine/view/writer~writer.breakContainer breakContainer} is that `breakAttributes` breaks all
* {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of given `position`, up to the first
* encountered {@link module:engine/view/containerelement~ContainerElement container element}. `breakContainer` assumes that given
* `position`
* is directly in container element and breaks that container element.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container`
* when {@link module:engine/view/range~Range#start start}
* and {@link module:engine/view/range~Range#end end} positions of a passed range are not placed inside same parent container.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element`
* when trying to break attributes
* inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element`
* when trying to break attributes
* inside {@link module:engine/view/uielement~UIElement UIElement}.
*
* @see module:engine/view/attributeelement~AttributeElement
* @see module:engine/view/containerelement~ContainerElement
* @see module:engine/view/writer~writer.breakContainer
* @function module:engine/view/writer~writer.breakAttributes
* @param {module:engine/view/position~Position|module:engine/view/range~Range} positionOrRange Position where to break attribute elements.
* @returns {module:engine/view/position~Position|module:engine/view/range~Range} New position or range, after breaking the attribute
* elements.
*/
function breakAttributes( positionOrRange ) {
if ( positionOrRange instanceof view_position_Position ) {
return _breakAttributes( positionOrRange );
} else {
return _breakAttributesRange( positionOrRange );
}
}
/**
* Breaks {@link module:engine/view/containerelement~ContainerElement container view element} into two, at the given position. Position
* has to be directly inside container element and cannot be in root. Does not break if position is at the beginning
* or at the end of it's parent element.
*
* <p>foo^bar</p> -> <p>foo</p><p>bar</p>
* <div><p>foo</p>^<p>bar</p></div> -> <div><p>foo</p></div><div><p>bar</p></div>
* <p>^foobar</p> -> ^<p>foobar</p>
* <p>foobar^</p> -> <p>foobar</p>^
*
* **Note:** Difference between {@link module:engine/view/writer~writer.breakAttributes breakAttributes} and
* {@link module:engine/view/writer~writer.breakContainer breakContainer} is that `breakAttributes` breaks all
* {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of given `position`, up to the first
* encountered {@link module:engine/view/containerelement~ContainerElement container element}. `breakContainer` assumes that given
* `position`
* is directly in container element and breaks that container element.
*
* @see module:engine/view/attributeelement~AttributeElement
* @see module:engine/view/containerelement~ContainerElement
* @see module:engine/view/writer~writer.breakAttributes
* @function module:engine/view/writer~writer.breakContainer
* @param {module:engine/view/position~Position} position Position where to break element.
* @returns {module:engine/view/position~Position} Position between broken elements. If element has not been broken, the returned position
* is placed either before it or after it.
*/
function breakContainer( position ) {
const element = position.parent;
if ( !( element.is( 'containerElement' ) ) ) {
/**
* Trying to break an element which is not a container element.
*
* @error view-writer-break-non-container-element
*/
throw new CKEditorError( 'view-writer-break-non-container-element: Trying to break an element which is not a container element.' );
}
if ( !element.parent ) {
/**
* Trying to break root element.
*
* @error view-writer-break-root
*/
throw new CKEditorError( 'view-writer-break-root: Trying to break root element.' );
}
if ( position.isAtStart ) {
return view_position_Position.createBefore( element );
} else if ( !position.isAtEnd ) {
const newElement = element.clone( false );
writer_insert( view_position_Position.createAfter( element ), newElement );
const sourceRange = new view_range_Range( position, view_position_Position.createAt( element, 'end' ) );
const targetPosition = new view_position_Position( newElement, 0 );
writer_move( sourceRange, targetPosition );
}
return view_position_Position.createAfter( element );
}
/**
* Merges {@link module:engine/view/attributeelement~AttributeElement attribute elements}. It also merges text nodes if needed.
* Only {@link module:engine/view/attributeelement~AttributeElement#isSimilar similar} attribute elements can be merged.
*
* In following examples `<p>` is a container and `<b>` is an attribute element:
*
* <p>foo[]bar</p> -> <p>foo{}bar</p>
* <p><b>foo</b>[]<b>bar</b></p> -> <p><b>foo{}bar</b></p>
* <p><b foo="bar">a</b>[]<b foo="baz">b</b></p> -> <p><b foo="bar">a</b>[]<b foo="baz">b</b></p>
*
* It will also take care about empty attributes when merging:
*
* <p><b>[]</b></p> -> <p>[]</p>
* <p><b>foo</b><i>[]</i><b>bar</b></p> -> <p><b>foo{}bar</b></p>
*
* **Note:** Difference between {@link module:engine/view/writer~writer.mergeAttributes mergeAttributes} and
* {@link module:engine/view/writer~writer.mergeContainers mergeContainers} is that `mergeAttributes` merges two
* {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
* while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
*
* @see module:engine/view/attributeelement~AttributeElement
* @see module:engine/view/containerelement~ContainerElement
* @see module:engine/view/writer~writer.mergeContainers
* @function module:engine/view/writer~writer.mergeAttributes
* @param {module:engine/view/position~Position} position Merge position.
* @returns {module:engine/view/position~Position} Position after merge.
*/
function mergeAttributes( position ) {
const positionOffset = position.offset;
const positionParent = position.parent;
// When inside text node - nothing to merge.
if ( positionParent.is( 'text' ) ) {
return position;
}
// When inside empty attribute - remove it.
if ( positionParent.is( 'attributeElement' ) && positionParent.childCount === 0 ) {
const parent = positionParent.parent;
const offset = positionParent.index;
positionParent.remove();
return mergeAttributes( new view_position_Position( parent, offset ) );
}
const nodeBefore = positionParent.getChild( positionOffset - 1 );
const nodeAfter = positionParent.getChild( positionOffset );
// Position should be placed between two nodes.
if ( !nodeBefore || !nodeAfter ) {
return position;
}
// When position is between two text nodes.
if ( nodeBefore.is( 'text' ) && nodeAfter.is( 'text' ) ) {
return mergeTextNodes( nodeBefore, nodeAfter );
}
// When selection is between two same attribute elements.
else if ( nodeBefore.is( 'attributeElement' ) && nodeAfter.is( 'attributeElement' ) && nodeBefore.isSimilar( nodeAfter ) ) {
// Move all children nodes from node placed after selection and remove that node.
const count = nodeBefore.childCount;
nodeBefore.appendChildren( nodeAfter.getChildren() );
nodeAfter.remove();
// New position is located inside the first node, before new nodes.
// Call this method recursively to merge again if needed.
return mergeAttributes( new view_position_Position( nodeBefore, count ) );
}
return position;
}
/**
* Merges two {@link module:engine/view/containerelement~ContainerElement container elements} that are before and after given position.
* Precisely, the element after the position is removed and it's contents are moved to element before the position.
*
* <p>foo</p>^<p>bar</p> -> <p>foo^bar</p>
* <div>foo</div>^<p>bar</p> -> <div>foo^bar</div>
*
* **Note:** Difference between {@link module:engine/view/writer~writer.mergeAttributes mergeAttributes} and
* {@link module:engine/view/writer~writer.mergeContainers mergeContainers} is that `mergeAttributes` merges two
* {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
* while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
*
* @see module:engine/view/attributeelement~AttributeElement
* @see module:engine/view/containerelement~ContainerElement
* @see module:engine/view/writer~writer.mergeAttributes
* @function module:engine/view/writer~writer.mergeContainers
* @param {module:engine/view/position~Position} position Merge position.
* @returns {module:engine/view/position~Position} Position after merge.
*/
function mergeContainers( position ) {
const prev = position.nodeBefore;
const next = position.nodeAfter;
if ( !prev || !next || !prev.is( 'containerElement' ) || !next.is( 'containerElement' ) ) {
/**
* Element before and after given position cannot be merged.
*
* @error view-writer-merge-containers-invalid-position
*/
throw new CKEditorError( 'view-writer-merge-containers-invalid-position: ' +
'Element before and after given position cannot be merged.' );
}
const lastChild = prev.getChild( prev.childCount - 1 );
const newPosition = lastChild instanceof view_text_Text ? view_position_Position.createAt( lastChild, 'end' ) : view_position_Position.createAt( prev, 'end' );
writer_move( view_range_Range.createIn( next ), view_position_Position.createAt( prev, 'end' ) );
writer_remove( view_range_Range.createOn( next ) );
return newPosition;
}
/**
* Insert node or nodes at specified position. Takes care about breaking attributes before insertion
* and merging them afterwards.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
* contains instances that are not {@link module:engine/view/text~Text Texts},
* {@link module:engine/view/attributeelement~AttributeElement AttributeElements},
* {@link module:engine/view/containerelement~ContainerElement ContainerElements},
* {@link module:engine/view/emptyelement~EmptyElement EmptyElements} or
* {@link module:engine/view/uielement~UIElement UIElements}.
*
* @function insert
* @param {module:engine/view/position~Position} position Insertion position.
* @param {module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement|
* module:engine/view/containerelement~ContainerElement|module:engine/view/emptyelement~EmptyElement|
* module:engine/view/uielement~UIElement|Iterable.<module:engine/view/text~Text|
* module:engine/view/attributeelement~AttributeElement|module:engine/view/containerelement~ContainerElement|
* module:engine/view/emptyelement~EmptyElement|module:engine/view/uielement~UIElement>} nodes Node or nodes to insert.
* @returns {module:engine/view/range~Range} Range around inserted nodes.
*/
function writer_insert( position, nodes ) {
nodes = isIterable( nodes ) ? [ ...nodes ] : [ nodes ];
// Check if nodes to insert are instances of AttributeElements, ContainerElements, EmptyElements, UIElements or Text.
validateNodesToInsert( nodes );
const container = getParentContainer( position );
if ( !container ) {
/**
* Position's parent container cannot be found.
*
* @error view-writer-invalid-position-container
*/
throw new CKEditorError( 'view-writer-invalid-position-container' );
}
const insertionPosition = _breakAttributes( position, true );
const length = container.insertChildren( insertionPosition.offset, nodes );
const endPosition = insertionPosition.getShiftedBy( length );
const start = mergeAttributes( insertionPosition );
// When no nodes were inserted - return collapsed range.
if ( length === 0 ) {
return new view_range_Range( start, start );
} else {
// If start position was merged - move end position.
if ( !start.isEqual( insertionPosition ) ) {
endPosition.offset--;
}
const end = mergeAttributes( endPosition );
return new view_range_Range( start, end );
}
}
/**
* Removes provided range from the container.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
* {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
* same parent container.
*
* @function module:engine/view/writer~writer.remove
* @param {module:engine/view/range~Range} range Range to remove from container. After removing, it will be updated
* to a collapsed range showing the new position.
* @returns {module:engine/view/documentfragment~DocumentFragment} Document fragment containing removed nodes.
*/
function writer_remove( range ) {
validateRangeContainer( range );
// If range is collapsed - nothing to remove.
if ( range.isCollapsed ) {
return new view_documentfragment_DocumentFragment();
}
// Break attributes at range start and end.
const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true );
const parentContainer = breakStart.parent;
const count = breakEnd.offset - breakStart.offset;
// Remove nodes in range.
const removed = parentContainer.removeChildren( breakStart.offset, count );
// Merge after removing.
const mergePosition = mergeAttributes( breakStart );
range.start = mergePosition;
range.end = view_position_Position.createFromPosition( mergePosition );
// Return removed nodes.
return new view_documentfragment_DocumentFragment( removed );
}
/**
* Removes matching elements from given range.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
* {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
* same parent container.
*
* @function module:engine/view/writer~writer.clear
* @param {module:engine/view/range~Range} range Range to clear.
* @param {module:engine/view/element~Element} element Element to remove.
*/
function clear( range, element ) {
validateRangeContainer( range );
// Create walker on given range.
// We walk backward because when we remove element during walk it modifies range end position.
const walker = range.getWalker( {
direction: 'backward',
ignoreElementEnd: true
} );
// Let's walk.
for ( const current of walker ) {
const item = current.item;
let rangeToRemove;
// When current item matches to the given element.
if ( item.is( 'element' ) && element.isSimilar( item ) ) {
// Create range on this element.
rangeToRemove = view_range_Range.createOn( item );
// When range starts inside Text or TextProxy element.
} else if ( !current.nextPosition.isAfter( range.start ) && ( item.is( 'text' ) || item.is( 'textProxy' ) ) ) {
// We need to check if parent of this text matches to given element.
const parentElement = item.getAncestors().find( ancestor => {
return ancestor.is( 'element' ) && element.isSimilar( ancestor );
} );
// If it is then create range inside this element.
if ( parentElement ) {
rangeToRemove = view_range_Range.createIn( parentElement );
}
}
// If we have found element to remove.
if ( rangeToRemove ) {
// We need to check if element range stick out of the given range and truncate if it is.
if ( rangeToRemove.end.isAfter( range.end ) ) {
rangeToRemove.end = range.end;
}
if ( rangeToRemove.start.isBefore( range.start ) ) {
rangeToRemove.start = range.start;
}
// At the end we remove range with found element.
writer_remove( rangeToRemove );
}
}
}
/**
* Moves nodes from provided range to target position.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
* {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
* same parent container.
*
* @function module:engine/view/writer~writer.move
* @param {module:engine/view/range~Range} sourceRange Range containing nodes to move.
* @param {module:engine/view/position~Position} targetPosition Position to insert.
* @returns {module:engine/view/range~Range} Range in target container. Inserted nodes are placed between
* {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions.
*/
function writer_move( sourceRange, targetPosition ) {
let nodes;
if ( targetPosition.isAfter( sourceRange.end ) ) {
targetPosition = _breakAttributes( targetPosition, true );
const parent = targetPosition.parent;
const countBefore = parent.childCount;
sourceRange = _breakAttributesRange( sourceRange, true );
nodes = writer_remove( sourceRange );
targetPosition.offset += ( parent.childCount - countBefore );
} else {
nodes = writer_remove( sourceRange );
}
return writer_insert( targetPosition, nodes );
}
/**
* Wraps elements within range with provided {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-invalid-range-container`
* when {@link module:engine/view/range~Range#start}
* and {@link module:engine/view/range~Range#end} positions are not placed inside same parent container.
* Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
* an instance of {module:engine/view/attributeelement~AttributeElement AttributeElement}.
*
* @function module:engine/view/writer~writer.wrap
* @param {module:engine/view/range~Range} range Range to wrap.
* @param {module:engine/view/attributeelement~AttributeElement} attribute Attribute element to use as wrapper.
*/
function wrap( range, attribute ) {
if ( !( attribute instanceof AttributeElement ) ) {
throw new CKEditorError( 'view-writer-wrap-invalid-attribute' );
}
validateRangeContainer( range );
// If range is collapsed - nothing to wrap.
if ( range.isCollapsed ) {
return range;
}
// Range around one element.
if ( range.end.isEqual( range.start.getShiftedBy( 1 ) ) ) {
const node = range.start.nodeAfter;
if ( node instanceof AttributeElement && wrapAttributeElement( attribute, node ) ) {
return range;
}
}
// Range is inside single attribute and spans on all children.
if ( rangeSpansOnAllChildren( range ) && wrapAttributeElement( attribute, range.start.parent ) ) {
const parent = range.start.parent.parent;
const index = range.start.parent.index;
return view_range_Range.createFromParentsAndOffsets( parent, index, parent, index + 1 );
}
// Break attributes at range start and end.
const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true );
const parentContainer = breakStart.parent;
// Unwrap children located between break points.
const unwrappedRange = unwrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute );
// Wrap all children with attribute.
const newRange = wrapChildren( parentContainer, unwrappedRange.start.offset, unwrappedRange.end.offset, attribute );
// Merge attributes at the both ends and return a new range.
const start = mergeAttributes( newRange.start );
// If start position was merged - move end position back.
if ( !start.isEqual( newRange.start ) ) {
newRange.end.offset--;
}
const end = mergeAttributes( newRange.end );
return new view_range_Range( start, end );
}
/**
* Wraps position with provided attribute. Returns new position after wrapping. This method will also merge newly
* added attribute with its siblings whenever possible.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
* an instance of {module:engine/view/attributeelement~AttributeElement AttributeElement}.
*
* @param {module:engine/view/position~Position} position
* @param {module:engine/view/attributeelement~AttributeElement} attribute
* @returns {module:engine/view/position~Position} New position after wrapping.
*/
function wrapPosition( position, attribute ) {
if ( !( attribute instanceof AttributeElement ) ) {
throw new CKEditorError( 'view-writer-wrap-invalid-attribute' );
}
// Return same position when trying to wrap with attribute similar to position parent.
if ( attribute.isSimilar( position.parent ) ) {
return movePositionToTextNode( view_position_Position.createFromPosition( position ) );
}
// When position is inside text node - break it and place new position between two text nodes.
if ( position.parent.is( 'text' ) ) {
position = breakTextNode( position );
}
// Create fake element that will represent position, and will not be merged with other attributes.
const fakePosition = new AttributeElement();
fakePosition.priority = Number.POSITIVE_INFINITY;
fakePosition.isSimilar = () => false;
// Insert fake element in position location.
position.parent.insertChildren( position.offset, fakePosition );
// Range around inserted fake attribute element.
const wrapRange = new view_range_Range( position, position.getShiftedBy( 1 ) );
// Wrap fake element with attribute (it will also merge if possible).
wrap( wrapRange, attribute );
// Remove fake element and place new position there.
const newPosition = new view_position_Position( fakePosition.parent, fakePosition.index );
fakePosition.remove();
// If position is placed between text nodes - merge them and return position inside.
const nodeBefore = newPosition.nodeBefore;
const nodeAfter = newPosition.nodeAfter;
if ( nodeBefore instanceof view_text_Text && nodeAfter instanceof view_text_Text ) {
return mergeTextNodes( nodeBefore, nodeAfter );
}
// If position is next to text node - move position inside.
return movePositionToTextNode( newPosition );
}
/**
* Unwraps nodes within provided range from attribute element.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
* {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
* same parent container.
*
* @param {module:engine/view/range~Range} range
* @param {module:engine/view/attributeelement~AttributeElement} element
*/
function unwrap( range, attribute ) {
if ( !( attribute instanceof AttributeElement ) ) {
/**
* Attribute element need to be instance of attribute element.
*
* @error view-writer-unwrap-invalid-attribute
*/
throw new CKEditorError( 'view-writer-unwrap-invalid-attribute' );
}
validateRangeContainer( range );
// If range is collapsed - nothing to unwrap.
if ( range.isCollapsed ) {
return range;
}
// Range around one element - check if AttributeElement can be unwrapped partially when it's not similar.
// For example:
// <b class="foo bar" title="baz"></b> unwrap with: <b class="foo"></p> result: <b class"bar" title="baz"></b>
if ( range.end.isEqual( range.start.getShiftedBy( 1 ) ) ) {
const node = range.start.nodeAfter;
// Unwrap single attribute element.
if ( !attribute.isSimilar( node ) && node instanceof AttributeElement && unwrapAttributeElement( attribute, node ) ) {
return range;
}
}
// Break attributes at range start and end.
const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true );
const parentContainer = breakStart.parent;
// Unwrap children located between break points.
const newRange = unwrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute );
// Merge attributes at the both ends and return a new range.
const start = mergeAttributes( newRange.start );
// If start position was merged - move end position back.
if ( !start.isEqual( newRange.start ) ) {
newRange.end.offset--;
}
const end = mergeAttributes( newRange.end );
return new view_range_Range( start, end );
}
/**
* Renames element by creating a copy of renamed element but with changed name and then moving contents of the
* old element to the new one. Keep in mind that this will invalidate all {@link module:engine/view/position~Position positions} which
* has renamed element as {@link module:engine/view/position~Position#parent a parent}.
*
* New element has to be created because `Element#tagName` property in DOM is readonly.
*
* Since this function creates a new element and removes the given one, the new element is returned to keep reference.
*
* @param {module:engine/view/containerelement~ContainerElement} viewElement Element to be renamed.
* @param {String} newName New name for element.
*/
function rename( viewElement, newName ) {
const newElement = new ContainerElement( newName, viewElement.getAttributes() );
writer_insert( view_position_Position.createAfter( viewElement ), newElement );
writer_move( view_range_Range.createIn( viewElement ), view_position_Position.createAt( newElement ) );
writer_remove( view_range_Range.createOn( viewElement ) );
return newElement;
}
/**
* Attribute element need to be instance of attribute element.
*
* @error view-writer-wrap-invalid-attribute
*/
// Returns first parent container of specified {@link module:engine/view/position~Position Position}.
// Position's parent node is checked as first, then next parents are checked.
// Note that {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} is treated like a container.
//
// @param {module:engine/view/position~Position} position Position used as a start point to locate parent container.
// @returns {module:engine/view/containerelement~ContainerElement|module:engine/view/documentfragment~DocumentFragment|undefined}
// Parent container element or `undefined` if container is not found.
function getParentContainer( position ) {
let parent = position.parent;
while ( !isContainerOrFragment( parent ) ) {
if ( !parent ) {
return undefined;
}
parent = parent.parent;
}
return parent;
}
// Function used by both public breakAttributes (without splitting text nodes) and by other methods (with
// splitting text nodes).
//
// @param {module:engine/view/range~Range} range Range which `start` and `end` positions will be used to break attributes.
// @param {Boolean} [forceSplitText = false] If set to `true`, will break text nodes even if they are directly in
// container element. This behavior will result in incorrect view state, but is needed by other view writing methods
// which then fixes view state. Defaults to `false`.
// @returns {module:engine/view/range~Range} New range with located at break positions.
function _breakAttributesRange( range, forceSplitText = false ) {
const rangeStart = range.start;
const rangeEnd = range.end;
validateRangeContainer( range );
// Break at the collapsed position. Return new collapsed range.
if ( range.isCollapsed ) {
const position = _breakAttributes( range.start, forceSplitText );
return new view_range_Range( position, position );
}
const breakEnd = _breakAttributes( rangeEnd, forceSplitText );
const count = breakEnd.parent.childCount;
const breakStart = _breakAttributes( rangeStart, forceSplitText );
// Calculate new break end offset.
breakEnd.offset += breakEnd.parent.childCount - count;
return new view_range_Range( breakStart, breakEnd );
}
// Function used by public breakAttributes (without splitting text nodes) and by other methods (with
// splitting text nodes).
//
// Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element` when break position
// is placed inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
//
// Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element` when break position
// is placed inside {@link module:engine/view/uielement~UIElement UIElement}.
//
// @param {module:engine/view/position~Position} position Position where to break attributes.
// @param {Boolean} [forceSplitText = false] If set to `true`, will break text nodes even if they are directly in
// container element. This behavior will result in incorrect view state, but is needed by other view writing methods
// which then fixes view state. Defaults to `false`.
// @returns {module:engine/view/position~Position} New position after breaking the attributes.
function _breakAttributes( position, forceSplitText = false ) {
const positionOffset = position.offset;
const positionParent = position.parent;
// If position is placed inside EmptyElement - throw an exception as we cannot break inside.
if ( position.parent.is( 'emptyElement' ) ) {
/**
* Cannot break inside EmptyElement instance.
*
* @error view-writer-cannot-break-empty-element
*/
throw new CKEditorError( 'view-writer-cannot-break-empty-element' );
}
// If position is placed inside UIElement - throw an exception as we cannot break inside.
if ( position.parent.is( 'uiElement' ) ) {
/**
* Cannot break inside UIElement instance.
*
* @error view-writer-cannot-break-ui-element
*/
throw new CKEditorError( 'view-writer-cannot-break-ui-element' );
}
// There are no attributes to break and text nodes breaking is not forced.
if ( !forceSplitText && positionParent.is( 'text' ) && isContainerOrFragment( positionParent.parent ) ) {
return view_position_Position.createFromPosition( position );
}
// Position's parent is container, so no attributes to break.
if ( isContainerOrFragment( positionParent ) ) {
return view_position_Position.createFromPosition( position );
}
// Break text and start again in new position.
if ( positionParent.is( 'text' ) ) {
return _breakAttributes( breakTextNode( position ), forceSplitText );
}
const length = positionParent.childCount;
// <p>foo<b><u>bar{}</u></b></p>
// <p>foo<b><u>bar</u>[]</b></p>
// <p>foo<b><u>bar</u></b>[]</p>
if ( positionOffset == length ) {
const newPosition = new view_position_Position( positionParent.parent, positionParent.index + 1 );
return _breakAttributes( newPosition, forceSplitText );
} else
// <p>foo<b><u>{}bar</u></b></p>
// <p>foo<b>[]<u>bar</u></b></p>
// <p>foo{}<b><u>bar</u></b></p>
if ( positionOffset === 0 ) {
const newPosition = new view_position_Position( positionParent.parent, positionParent.index );
return _breakAttributes( newPosition, forceSplitText );
}
// <p>foo<b><u>b{}ar</u></b></p>
// <p>foo<b><u>b[]ar</u></b></p>
// <p>foo<b><u>b</u>[]<u>ar</u></b></p>
// <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
else {
const offsetAfter = positionParent.index + 1;
// Break element.
const clonedNode = positionParent.clone();
// Insert cloned node to position's parent node.
positionParent.parent.insertChildren( offsetAfter, clonedNode );
// Get nodes to move.
const count = positionParent.childCount - positionOffset;
const nodesToMove = positionParent.removeChildren( positionOffset, count );
// Move nodes to cloned node.
clonedNode.appendChildren( nodesToMove );
// Create new position to work on.
const newPosition = new view_position_Position( positionParent.parent, offsetAfter );
return _breakAttributes( newPosition, forceSplitText );
}
}
// Unwraps children from provided `attribute`. Only children contained in `parent` element between
// `startOffset` and `endOffset` will be unwrapped.
//
// @param {module:engine/view/element~Element} parent
// @param {Number} startOffset
// @param {Number} endOffset
// @param {module:engine/view/element~Element} attribute
function unwrapChildren( parent, startOffset, endOffset, attribute ) {
let i = startOffset;
const unwrapPositions = [];
// Iterate over each element between provided offsets inside parent.
while ( i < endOffset ) {
const child = parent.getChild( i );
// If attributes are the similar, then unwrap.
if ( child.isSimilar( attribute ) ) {
const unwrapped = child.getChildren();
const count = child.childCount;
// Replace wrapper element with its children
child.remove();
parent.insertChildren( i, unwrapped );
// Save start and end position of moved items.
unwrapPositions.push(
new view_position_Position( parent, i ),
new view_position_Position( parent, i + count )
);
// Skip elements that were unwrapped. Assuming that there won't be another element to unwrap in child
// elements.
i += count;
endOffset += count - 1;
} else {
// If other nested attribute is found start unwrapping there.
if ( child.is( 'attributeElement' ) ) {
unwrapChildren( child, 0, child.childCount, attribute );
}
i++;
}
}
// Merge at each unwrap.
let offsetChange = 0;
for ( const position of unwrapPositions ) {
position.offset -= offsetChange;
// Do not merge with elements outside selected children.
if ( position.offset == startOffset || position.offset == endOffset ) {
continue;
}
const newPosition = mergeAttributes( position );
// If nodes were merged - other merge offsets will change.
if ( !newPosition.isEqual( position ) ) {
offsetChange++;
endOffset--;
}
}
return view_range_Range.createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
}
// Wraps children with provided `attribute`. Only children contained in `parent` element between
// `startOffset` and `endOffset` will be wrapped.
//
// @param {module:engine/view/element~Element} parent
// @param {Number} startOffset
// @param {Number} endOffset
// @param {module:engine/view/element~Element} attribute
function wrapChildren( parent, startOffset, endOffset, attribute ) {
let i = startOffset;
const wrapPositions = [];
while ( i < endOffset ) {
const child = parent.getChild( i );
const isText = child.is( 'text' );
const isAttribute = child.is( 'attributeElement' );
const isEmpty = child.is( 'emptyElement' );
const isUI = child.is( 'uiElement' );
// Wrap text, empty elements, ui elements or attributes with higher or equal priority.
if ( isText || isEmpty || isUI || ( isAttribute && shouldABeOutsideB( attribute, child ) ) ) {
// Clone attribute.
const newAttribute = attribute.clone();
// Wrap current node with new attribute;
child.remove();
newAttribute.appendChildren( child );
parent.insertChildren( i, newAttribute );
wrapPositions.push( new view_position_Position( parent, i ) );
}
// If other nested attribute is found start wrapping there.
else if ( isAttribute ) {
wrapChildren( child, 0, child.childCount, attribute );
}
i++;
}
// Merge at each wrap.
let offsetChange = 0;
for ( const position of wrapPositions ) {
position.offset -= offsetChange;
// Do not merge with elements outside selected children.
if ( position.offset == startOffset ) {
continue;
}
const newPosition = mergeAttributes( position );
// If nodes were merged - other merge offsets will change.
if ( !newPosition.isEqual( position ) ) {
offsetChange++;
endOffset--;
}
}
return view_range_Range.createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
}
// Checks if first {@link module:engine/view/attributeelement~AttributeElement AttributeElement} provided to the function
// can be wrapped otuside second element. It is done by comparing elements'
// {@link module:engine/view/attributeelement~AttributeElement#priority priorities}, if both have same priority
// {@link module:engine/view/element~Element#getIdentity identities} are compared.
//
// @param {module:engine/view/attributeelement~AttributeElement} a
// @param {module:engine/view/attributeelement~AttributeElement} b
// @returns {Boolean}
function shouldABeOutsideB( a, b ) {
if ( a.priority < b.priority ) {
return true;
} else if ( a.priority > b.priority ) {
return false;
}
// When priorities are equal and names are different - use identities.
return a.getIdentity() < b.getIdentity();
}
// Returns new position that is moved to near text node. Returns same position if there is no text node before of after
// specified position.
//
// <p>foo[]</p> -> <p>foo{}</p>
// <p>[]foo</p> -> <p>{}foo</p>
//
// @param {module:engine/view/position~Position} position
// @returns {module:engine/view/position~Position} Position located inside text node or same position if there is no text nodes
// before or after position location.
function movePositionToTextNode( position ) {
const nodeBefore = position.nodeBefore;
if ( nodeBefore && nodeBefore.is( 'text' ) ) {
return new view_position_Position( nodeBefore, nodeBefore.data.length );
}
const nodeAfter = position.nodeAfter;
if ( nodeAfter && nodeAfter.is( 'text' ) ) {
return new view_position_Position( nodeAfter, 0 );
}
return position;
}
// Breaks text node into two text nodes when possible.
//
// <p>foo{}bar</p> -> <p>foo[]bar</p>
// <p>{}foobar</p> -> <p>[]foobar</p>
// <p>foobar{}</p> -> <p>foobar[]</p>
//
// @param {module:engine/view/position~Position} position Position that need to be placed inside text node.
// @returns {module:engine/view/position~Position} New position after breaking text node.
function breakTextNode( position ) {
if ( position.offset == position.parent.data.length ) {
return new view_position_Position( position.parent.parent, position.parent.index + 1 );
}
if ( position.offset === 0 ) {
return new view_position_Position( position.parent.parent, position.parent.index );
}
// Get part of the text that need to be moved.
const textToMove = position.parent.data.slice( position.offset );
// Leave rest of the text in position's parent.
position.parent.data = position.parent.data.slice( 0, position.offset );
// Insert new text node after position's parent text node.
position.parent.parent.insertChildren( position.parent.index + 1, new view_text_Text( textToMove ) );
// Return new position between two newly created text nodes.
return new view_position_Position( position.parent.parent, position.parent.index + 1 );
}
// Merges two text nodes into first node. Removes second node and returns merge position.
//
// @param {module:engine/view/text~Text} t1 First text node to merge. Data from second text node will be moved at the end of
// this text node.
// @param {module:engine/view/text~Text} t2 Second text node to merge. This node will be removed after merging.
// @returns {module:engine/view/position~Position} Position after merging text nodes.
function mergeTextNodes( t1, t2 ) {
// Merge text data into first text node and remove second one.
const nodeBeforeLength = t1.data.length;
t1.data += t2.data;
t2.remove();
return new view_position_Position( t1, nodeBeforeLength );
}
// Wraps one {@link module:engine/view/attributeelement~AttributeElement AttributeElement} into another by merging them if possible.
// When merging is possible - all attributes, styles and classes are moved from wrapper element to element being
// wrapped.
//
// @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement.
// @param {module:engine/view/attributeelement~AttributeElement} toWrap AttributeElement to wrap using wrapper element.
// @returns {Boolean} Returns `true` if elements are merged.
function wrapAttributeElement( wrapper, toWrap ) {
// Can't merge if name or priority differs.
if ( wrapper.name !== toWrap.name || wrapper.priority !== toWrap.priority ) {
return false;
}
// Check if attributes can be merged.
for ( const key of wrapper.getAttributeKeys() ) {
// Classes and styles should be checked separately.
if ( key === 'class' || key === 'style' ) {
continue;
}
// If some attributes are different we cannot wrap.
if ( toWrap.hasAttribute( key ) && toWrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) {
return false;
}
}
// Check if styles can be merged.
for ( const key of wrapper.getStyleNames() ) {
if ( toWrap.hasStyle( key ) && toWrap.getStyle( key ) !== wrapper.getStyle( key ) ) {
return false;
}
}
// Move all attributes/classes/styles from wrapper to wrapped AttributeElement.
for ( const key of wrapper.getAttributeKeys() ) {
// Classes and styles should be checked separately.
if ( key === 'class' || key === 'style' ) {
continue;
}
// Move only these attributes that are not present - other are similar.
if ( !toWrap.hasAttribute( key ) ) {
toWrap.setAttribute( key, wrapper.getAttribute( key ) );
}
}
for ( const key of wrapper.getStyleNames() ) {
if ( !toWrap.hasStyle( key ) ) {
toWrap.setStyle( key, wrapper.getStyle( key ) );
}
}
for ( const key of wrapper.getClassNames() ) {
if ( !toWrap.hasClass( key ) ) {
toWrap.addClass( key );
}
}
return true;
}
// Unwraps {@link module:engine/view/attributeelement~AttributeElement AttributeElement} from another by removing corresponding attributes,
// classes and styles. All attributes, classes and styles from wrapper should be present inside element being unwrapped.
//
// @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement.
// @param {module:engine/view/attributeelement~AttributeElement} toUnwrap AttributeElement to unwrap using wrapper element.
// @returns {Boolean} Returns `true` if elements are unwrapped.
function unwrapAttributeElement( wrapper, toUnwrap ) {
// Can't unwrap if name or priority differs.
if ( wrapper.name !== toUnwrap.name || wrapper.priority !== toUnwrap.priority ) {
return false;
}
// Check if AttributeElement has all wrapper attributes.
for ( const key of wrapper.getAttributeKeys() ) {
// Classes and styles should be checked separately.
if ( key === 'class' || key === 'style' ) {
continue;
}
// If some attributes are missing or different we cannot unwrap.
if ( !toUnwrap.hasAttribute( key ) || toUnwrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) {
return false;
}
}
// Check if AttributeElement has all wrapper classes.
if ( !toUnwrap.hasClass( ...wrapper.getClassNames() ) ) {
return false;
}
// Check if AttributeElement has all wrapper styles.
for ( const key of wrapper.getStyleNames() ) {
// If some styles are missing or different we cannot unwrap.
if ( !toUnwrap.hasStyle( key ) || toUnwrap.getStyle( key ) !== wrapper.getStyle( key ) ) {
return false;
}
}
// Remove all wrapper's attributes from unwrapped element.
for ( const key of wrapper.getAttributeKeys() ) {
// Classes and styles should be checked separately.
if ( key === 'class' || key === 'style' ) {
continue;
}
toUnwrap.removeAttribute( key );
}
// Remove all wrapper's classes from unwrapped element.
toUnwrap.removeClass( ...wrapper.getClassNames() );
// Remove all wrapper's styles from unwrapped element.
toUnwrap.removeStyle( ...wrapper.getStyleNames() );
return true;
}
// Returns `true` if range is located in same {@link module:engine/view/attributeelement~AttributeElement AttributeElement}
// (`start` and `end` positions are located inside same {@link module:engine/view/attributeelement~AttributeElement AttributeElement}),
// starts on 0 offset and ends after last child node.
//
// @param {module:engine/view/range~Range} Range
// @returns {Boolean}
function rangeSpansOnAllChildren( range ) {
return range.start.parent == range.end.parent && range.start.parent.is( 'attributeElement' ) &&
range.start.offset === 0 && range.end.offset === range.start.parent.childCount;
}
// Checks if provided nodes are valid to insert. Checks if each node is an instance of
// {@link module:engine/view/text~Text Text} or {@link module:engine/view/attributeelement~AttributeElement AttributeElement},
// {@link module:engine/view/containerelement~ContainerElement ContainerElement},
// {@link module:engine/view/emptyelement~EmptyElement EmptyElement} or
// {@link module:engine/view/uielement~UIElement UIElement}.
//
// Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
// contains instances that are not {@link module:engine/view/text~Text Texts},
// {@link module:engine/view/emptyelement~EmptyElement EmptyElements},
// {@link module:engine/view/uielement~UIElement UIElements},
// {@link module:engine/view/attributeelement~AttributeElement AttributeElements} or
// {@link module:engine/view/containerelement~ContainerElement ContainerElements}.
//
// @param Iterable.<module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement
// |module:engine/view/containerelement~ContainerElement> nodes
function validateNodesToInsert( nodes ) {
for ( const node of nodes ) {
if ( !validNodesToInsert.some( ( validNode => node instanceof validNode ) ) ) { // eslint-disable-line no-use-before-define
/**
* Inserted nodes should be valid to insert. of {@link module:engine/view/attributeelement~AttributeElement AttributeElement},
* {@link module:engine/view/containerelement~ContainerElement ContainerElement},
* {@link module:engine/view/emptyelement~EmptyElement EmptyElement},
* {@link module:engine/view/uielement~UIElement UIElement}, {@link module:engine/view/text~Text Text}.
*
* @error view-writer-insert-invalid-node
*/
throw new CKEditorError( 'view-writer-insert-invalid-node' );
}
if ( !node.is( 'text' ) ) {
validateNodesToInsert( node.getChildren() );
}
}
}
const validNodesToInsert = [ view_text_Text, AttributeElement, ContainerElement, emptyelement_EmptyElement, uielement_UIElement ];
// Checks if node is ContainerElement or DocumentFragment, because in most cases they should be treated the same way.
//
// @param {module:engine/view/node~Node} node
// @returns {Boolean} Returns `true` if node is instance of ContainerElement or DocumentFragment.
function isContainerOrFragment( node ) {
return node && ( node.is( 'containerElement' ) || node.is( 'documentFragment' ) );
}
// Checks if {@link module:engine/view/range~Range#start range start} and {@link module:engine/view/range~Range#end range end} are placed
// inside same {@link module:engine/view/containerelement~ContainerElement container element}.
// Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when validation fails.
//
// @param {module:engine/view/range~Range} range
function validateRangeContainer( range ) {
const startContainer = getParentContainer( range.start );
const endContainer = getParentContainer( range.end );
if ( !startContainer || !endContainer || startContainer !== endContainer ) {
/**
* Range container is invalid. This can happen if {@link module:engine/view/range~Range#start range start} and
* {@link module:engine/view/range~Range#end range end} positions are not placed inside same container or
* parent container for these positions cannot be found.
*
* @error view-writer-invalid-range-container
*/
throw new CKEditorError( 'view-writer-invalid-range-container' );
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/conversion/model-to-view-converters.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* Contains {@link module:engine/model/model model} to {@link module:engine/view/view view} converters for
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}.
*
* @module engine/conversion/model-to-view-converters
*/
/**
* Function factory, creates a converter that converts node insertion changes from the model to the view.
* The view element that will be added to the view depends on passed parameter. If {@link module:engine/view/element~Element} was passed,
* it will be cloned and the copy will be inserted. If `Function` is provided, it is passed all the parameters of the
* dispatcher's {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert insert event}.
* It's expected that the function returns a {@link module:engine/view/element~Element}.
* The result of the function will be inserted to the view.
*
* The converter automatically consumes corresponding value from consumables list, stops the event (see
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}) and bind model and view elements.
*
* modelDispatcher.on( 'insert:paragraph', insertElement( new ViewElement( 'p' ) ) );
*
* modelDispatcher.on(
* 'insert:myElem',
* insertElement( ( data, consumable, conversionApi ) => {
* let myElem = new ViewElement( 'myElem', { myAttr: true }, new ViewText( 'myText' ) );
*
* // Do something fancy with myElem using data/consumable/conversionApi ...
*
* return myElem;
* }
* ) );
*
* @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which
* will be inserted.
* @returns {Function} Insert element event converter.
*/
function insertElement( elementCreator ) {
return ( evt, data, consumable, conversionApi ) => {
const viewElement = ( elementCreator instanceof view_element_Element ) ?
elementCreator.clone( true ) :
elementCreator( data, consumable, conversionApi );
if ( !viewElement ) {
return;
}
if ( !consumable.consume( data.item, 'insert' ) ) {
return;
}
const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
conversionApi.mapper.bindElements( data.item, viewElement );
view_writer.insert( viewPosition, viewElement );
};
}
/**
* Function factory, creates a default model-to-view converter for text insertion changes.
*
* The converter automatically consumes corresponding value from consumables list and stops the event (see
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
*
* modelDispatcher.on( 'insert:$text', insertText() );
*
* @returns {Function} Insert text event converter.
*/
function model_to_view_converters_insertText() {
return ( evt, data, consumable, conversionApi ) => {
if ( !consumable.consume( data.item, 'insert' ) ) {
return;
}
const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
const viewText = new view_text_Text( data.item.data );
view_writer.insert( viewPosition, viewText );
};
}
/**
* Function factory, creates a converter that converts marker adding change to the view ui element.
* The view ui element that will be added to the view depends on passed parameter. See {@link ~insertElement}.
* In a case of collapsed range element will not wrap range but separate elements will be placed at the beginning
* and at the end of the range.
*
* **Note:** unlike {@link ~insertElement}, the converter does not bind view element to model, because this converter
* uses marker as "model source of data". This means that view ui element does not have corresponding model element.
*
* @param {module:engine/view/uielement~UIElement|Function} elementCreator View ui element, or function returning a view element, which
* will be inserted.
* @returns {Function} Insert element event converter.
*/
function insertUIElement( elementCreator ) {
return ( evt, data, consumable, conversionApi ) => {
let viewStartElement, viewEndElement;
if ( elementCreator instanceof view_element_Element ) {
viewStartElement = elementCreator.clone( true );
viewEndElement = elementCreator.clone( true );
} else {
data.isOpening = true;
viewStartElement = elementCreator( data, consumable, conversionApi );
data.isOpening = false;
viewEndElement = elementCreator( data, consumable, conversionApi );
}
if ( !viewStartElement || !viewEndElement ) {
return;
}
const markerRange = data.markerRange;
const eventName = evt.name;
// Marker that is collapsed has consumable build differently that non-collapsed one.
// For more information see `addMarker` and `removeMarker` events description.
// If marker's range is collapsed - check if it can be consumed.
if ( markerRange.isCollapsed && !consumable.consume( markerRange, eventName ) ) {
return;
}
// If marker's range is not collapsed - consume all items inside.
for ( const value of markerRange ) {
if ( !consumable.consume( value.item, eventName ) ) {
return;
}
}
const mapper = conversionApi.mapper;
view_writer.insert( mapper.toViewPosition( markerRange.start ), viewStartElement );
if ( !markerRange.isCollapsed ) {
view_writer.insert( mapper.toViewPosition( markerRange.end ), viewEndElement );
}
};
}
/**
* Function factory, creates a converter that converts set/change attribute changes from the model to the view. Attributes
* from model are converted to the view element attributes in the view. You may provide a custom function to generate a
* key-value attribute pair to add/change. If not provided, model attributes will be converted to view elements attributes
* on 1-to-1 basis.
*
* **Note:** Provided attribute creator should always return the same `key` for given attribute from the model.
*
* The converter automatically consumes corresponding value from consumables list and stops the event (see
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
*
* modelDispatcher.on( 'addAttribute:customAttr:myElem', setAttribute( ( data ) => {
* // Change attribute key from `customAttr` to `class` in view.
* const key = 'class';
* let value = data.attributeNewValue;
*
* // Force attribute value to 'empty' if the model element is empty.
* if ( data.item.childCount === 0 ) {
* value = 'empty';
* }
*
* // Return key-value pair.
* return { key, value };
* } ) );
*
* @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
* represents attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
* The function is passed all the parameters of the
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute}
* or {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:changeAttribute} event.
* @returns {Function} Set/change attribute converter.
*/
function setAttribute( attributeCreator ) {
attributeCreator = attributeCreator || ( ( value, key ) => ( { value, key } ) );
return ( evt, data, consumable, conversionApi ) => {
if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
return;
}
const { key, value } = attributeCreator( data.attributeNewValue, data.attributeKey, data, consumable, conversionApi );
conversionApi.mapper.toViewElement( data.item ).setAttribute( key, value );
};
}
/**
* Function factory, creates a converter that converts remove attribute changes from the model to the view. Removes attributes
* that were converted to the view element attributes in the view. You may provide a custom function to generate a
* key-value attribute pair to remove. If not provided, model attributes will be removed from view elements on 1-to-1 basis.
*
* **Note:** Provided attribute creator should always return the same `key` for given attribute from the model.
*
* **Note:** You can use the same attribute creator as in {@link module:engine/conversion/model-to-view-converters~setAttribute}.
*
* The converter automatically consumes corresponding value from consumables list and stops the event (see
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
*
* modelDispatcher.on( 'removeAttribute:customAttr:myElem', removeAttribute( ( data ) => {
* // Change attribute key from `customAttr` to `class` in view.
* const key = 'class';
* let value = data.attributeNewValue;
*
* // Force attribute value to 'empty' if the model element is empty.
* if ( data.item.childCount === 0 ) {
* value = 'empty';
* }
*
* // Return key-value pair.
* return { key, value };
* } ) );
*
* @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
* represents attribute key and attribute value to be removed from {@link module:engine/view/element~Element view element}.
* The function is passed all the parameters of the
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute addAttribute event}
* or {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:changeAttribute changeAttribute event}.
* @returns {Function} Remove attribute converter.
*/
function removeAttribute( attributeCreator ) {
attributeCreator = attributeCreator || ( ( value, key ) => ( { key } ) );
return ( evt, data, consumable, conversionApi ) => {
if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
return;
}
const { key } = attributeCreator( data.attributeOldValue, data.attributeKey, data, consumable, conversionApi );
conversionApi.mapper.toViewElement( data.item ).removeAttribute( key );
};
}
/**
* Function factory, creates a converter that converts set/change attribute changes from the model to the view. In this case,
* model attributes are converted to a view element that will be wrapping view nodes which corresponding model nodes had
* the attribute set. This is useful for attributes like `bold`, which may be set on text nodes in model but are
* represented as an element in the view:
*
* [paragraph] MODEL ====> VIEW <p>
* |- a {bold: true} |- <b>
* |- b {bold: true} | |- ab
* |- c |- c
*
* The wrapping node depends on passed parameter. If {@link module:engine/view/element~Element} was passed, it will be cloned and
* the copy will become the wrapping element. If `Function` is provided, it is passed attribute value and then all the parameters of the
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute addAttribute event}.
* It's expected that the function returns a {@link module:engine/view/element~Element}.
* The result of the function will be the wrapping element.
* When provided `Function` does not return element, then will be no conversion.
*
* The converter automatically consumes corresponding value from consumables list, stops the event (see
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
*
* modelDispatcher.on( 'addAttribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
*
* @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will
* be used for wrapping.
* @returns {Function} Set/change attribute converter.
*/
function wrapItem( elementCreator ) {
return ( evt, data, consumable, conversionApi ) => {
const viewElement = ( elementCreator instanceof view_element_Element ) ?
elementCreator.clone( true ) :
elementCreator( data.attributeNewValue, data, consumable, conversionApi );
if ( !viewElement ) {
return;
}
if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
return;
}
let viewRange = conversionApi.mapper.toViewRange( data.range );
// If this is a change event (because old value is not empty) and the creator is a function (so
// it may create different view elements basing on attribute value) we have to create
// view element basing on old value and unwrap it before wrapping with a newly created view element.
if ( data.attributeOldValue !== null && !( elementCreator instanceof view_element_Element ) ) {
const oldViewElement = elementCreator( data.attributeOldValue, data, consumable, conversionApi );
viewRange = view_writer.unwrap( viewRange, oldViewElement );
}
view_writer.wrap( viewRange, viewElement );
};
}
/**
* Function factory, creates a converter that converts remove attribute changes from the model to the view. It assumes, that
* attributes from model were converted to elements in the view. This converter will unwrap view nodes from corresponding
* view element if given attribute was removed.
*
* The view element type that will be unwrapped depends on passed parameter.
* If {@link module:engine/view/element~Element} was passed, it will be used to look for similar element in the view for unwrapping.
* If `Function` is provided, it is passed all the parameters of the
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute addAttribute event}.
* It's expected that the function returns a {@link module:engine/view/element~Element}.
* The result of the function will be used to look for similar element in the view for unwrapping.
*
* The converter automatically consumes corresponding value from consumables list, stops the event (see
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}) and bind model and view elements.
*
* modelDispatcher.on( 'removeAttribute:bold', unwrapItem( new ViewAttributeElement( 'strong' ) ) );
*
* @see module:engine/conversion/model-to-view-converters~wrapItem
* @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will
* be used for unwrapping.
* @returns {Function} Remove attribute converter.
*/
function unwrapItem( elementCreator ) {
return ( evt, data, consumable, conversionApi ) => {
const viewElement = ( elementCreator instanceof view_element_Element ) ?
elementCreator.clone( true ) :
elementCreator( data.attributeOldValue, data, consumable, conversionApi );
if ( !viewElement ) {
return;
}
if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
return;
}
const viewRange = conversionApi.mapper.toViewRange( data.range );
view_writer.unwrap( viewRange, viewElement );
};
}
/**
* Function factory, creates a default model-to-view converter for node remove changes.
*
* modelDispatcher.on( 'remove', remove() );
*
* @returns {Function} Remove event converter.
*/
function model_to_view_converters_remove() {
return ( evt, data, consumable, conversionApi ) => {
if ( !consumable.consume( data.item, 'remove' ) ) {
return;
}
// We cannot map non-existing positions from model to view. Since a range was removed
// from the model, we cannot recreate that range and map it to view, because
// end of that range is incorrect.
// Instead we will use `data.sourcePosition` as this is the last correct model position and
// it is a position before the removed item. Then, we will calculate view range to remove "manually".
let viewPosition = conversionApi.mapper.toViewPosition( data.sourcePosition );
let viewRange;
if ( data.item.is( 'element' ) ) {
// Note: in remove conversion we cannot use model-to-view element mapping because `data.item` may be
// already mapped to another element (this happens when move change is converted).
// In this case however, `viewPosition` is the position before view element that corresponds to removed model element.
//
// First, fix the position. Traverse the tree forward until the container element is found. The `viewPosition`
// may be before a ui element, before attribute element or at the end of text element.
viewPosition = viewPosition.getLastMatchingPosition( value => !value.item.is( 'containerElement' ) );
if ( viewPosition.parent.is( 'text' ) && viewPosition.isAtEnd ) {
viewPosition = view_position_Position.createAfter( viewPosition.parent );
}
viewRange = view_range_Range.createOn( viewPosition.nodeAfter );
} else {
// If removed item is a text node, we need to traverse view tree to find the view range to remove.
// Range to remove will start `viewPosition` and should contain amount of characters equal to the amount of removed characters.
const viewRangeEnd = _shiftViewPositionByCharacters( viewPosition, data.item.offsetSize );
viewRange = new view_range_Range( viewPosition, viewRangeEnd );
}
// Trim the range to remove in case some UI elements are on the view range boundaries.
view_writer.remove( viewRange.getTrimmed() );
// Unbind this element only if it was moved to graveyard.
// The dispatcher#remove event will also be fired if the element was moved to another place (remove+insert are fired).
// Let's say that <b> is moved before <a>. The view will be changed like this:
//
// 1) start: <a></a><b></b>
// 2) insert: <b (new)></b><a></a><b></b>
// 3) remove: <b (new)></b><a></a>
//
// If we'll unbind the <b> element in step 3 we'll also lose binding of the <b (new)> element in the view,
// because unbindModelElement() cancels both bindings – (model <b> => view <b (new)>) and (view <b (new)> => model <b>).
// We can't lose any of these.
//
// See #847.
if ( data.item.root.rootName == '$graveyard' ) {
conversionApi.mapper.unbindModelElement( data.item );
}
};
}
/**
* Function factory, creates converter that converts all texts inside marker's range. Converter wraps each text with
* {@link module:engine/view/attributeelement~AttributeElement} created from provided descriptor.
* See {link module:engine/conversion/model-to-view-converters~highlightDescriptorToAttributeElement}.
*
* @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor|Function} highlightDescriptor
* @return {Function}
*/
function highlightText( highlightDescriptor ) {
return ( evt, data, consumable, conversionApi ) => {
const descriptor = typeof highlightDescriptor == 'function' ?
highlightDescriptor( data, consumable, conversionApi ) :
highlightDescriptor;
const modelItem = data.item;
if ( !descriptor || data.markerRange.isCollapsed || !modelItem.is( 'textProxy' ) ) {
return;
}
if ( !consumable.consume( modelItem, evt.name ) ) {
return;
}
if ( !descriptor.id ) {
descriptor.id = data.markerName;
}
const viewElement = createViewElementFromHighlightDescriptor( descriptor );
const viewRange = conversionApi.mapper.toViewRange( data.range );
if ( evt.name.split( ':' )[ 0 ] == 'addMarker' ) {
view_writer.wrap( viewRange, viewElement );
} else {
view_writer.unwrap( viewRange, viewElement );
}
};
}
/**
* Converter function factory. Creates a function which applies the marker's highlight to all elements inside a marker's range.
* The converter checks if an element has the addHighlight and removeHighlight functions stored as
* {@link module:engine/view/element~Element#setCustomProperty custom properties} and if so use them to apply the highlight.
* In such case converter will consume all element's children, assuming that they were handled by element itself.
* If the highlight descriptor will not provide priority, priority `10` will be used as default, to be compliant with
* {@link module:engine/conversion/model-to-view-converters~highlightText} method which uses default priority of
* {@link module:engine/view/attributeelement~AttributeElement}.
*
* If the highlight descriptor will not provide `id` property, name of the marker will be used.
* When `addHighlight` and `removeHighlight` custom properties are not present, element is not converted
* in any special way. This means that converters will proceed to convert element's child nodes.
*
* @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor|Function} highlightDescriptor
* @return {Function}
*/
function highlightElement( highlightDescriptor ) {
return ( evt, data, consumable, conversionApi ) => {
const descriptor = typeof highlightDescriptor == 'function' ?
highlightDescriptor( data, consumable, conversionApi ) :
highlightDescriptor;
const modelItem = data.item;
if ( !descriptor || data.markerRange.isCollapsed || !modelItem.is( 'element' ) ) {
return;
}
if ( !consumable.test( data.item, evt.name ) ) {
return;
}
if ( !descriptor.priority ) {
descriptor.priority = 10;
}
if ( !descriptor.id ) {
descriptor.id = data.markerName;
}
const viewElement = conversionApi.mapper.toViewElement( modelItem );
const addMarker = evt.name.split( ':' )[ 0 ] == 'addMarker';
const highlightHandlingMethod = addMarker ? 'addHighlight' : 'removeHighlight';
if ( viewElement && viewElement.getCustomProperty( highlightHandlingMethod ) ) {
// Consume element itself.
consumable.consume( data.item, evt.name );
// Consume all children nodes.
for ( const value of range_Range.createIn( modelItem ) ) {
consumable.consume( value.item, evt.name );
}
viewElement.getCustomProperty( highlightHandlingMethod )( viewElement, addMarker ? descriptor : descriptor.id );
}
};
}
/**
* Function factory, creates a default model-to-view converter for removing {@link module:engine/view/uielement~UIElement ui element}
* basing on marker remove change.
*
* @param {module:engine/view/uielement~UIElement|Function} elementCreator View ui element, or function returning
* a view ui element, which will be used as a pattern when look for element to remove at the marker start position.
* @returns {Function} Remove ui element converter.
*/
function removeUIElement( elementCreator ) {
return ( evt, data, consumable, conversionApi ) => {
let viewStartElement, viewEndElement;
if ( elementCreator instanceof view_element_Element ) {
viewStartElement = elementCreator.clone( true );
viewEndElement = elementCreator.clone( true );
} else {
data.isOpening = true;
viewStartElement = elementCreator( data, consumable, conversionApi );
data.isOpening = false;
viewEndElement = elementCreator( data, consumable, conversionApi );
}
if ( !viewStartElement || !viewEndElement ) {
return;
}
const markerRange = data.markerRange;
const eventName = evt.name;
// If marker's range is collapsed - check if it can be consumed.
if ( markerRange.isCollapsed && !consumable.consume( markerRange, eventName ) ) {
return;
}
// Check if all items in the range can be consumed, and consume them.
for ( const value of markerRange ) {
if ( !consumable.consume( value.item, eventName ) ) {
return;
}
}
const viewRange = conversionApi.mapper.toViewRange( markerRange );
// First remove closing element.
view_writer.clear( viewRange.getEnlarged(), viewEndElement );
// If closing and opening elements are not the same then remove opening element.
if ( !viewStartElement.isSimilar( viewEndElement ) ) {
view_writer.clear( viewRange.getEnlarged(), viewStartElement );
}
};
}
/**
* Returns the consumable type that is to be consumed in an event, basing on that event name.
*
* @param {String} evtName Event name.
* @returns {String} Consumable type.
*/
function eventNameToConsumableType( evtName ) {
const parts = evtName.split( ':' );
return parts[ 0 ] + ':' + parts[ 1 ];
}
// Helper function that shifts given view `position` in a way that returned position is after `howMany` characters compared
// to the original `position`.
// Because in view there might be view ui elements splitting text nodes, we cannot simply use `ViewPosition#getShiftedBy()`.
function _shiftViewPositionByCharacters( position, howMany ) {
// Create a walker that will walk the view tree starting from given position and walking characters one-by-one.
const walker = new view_treewalker_TreeWalker( { startPosition: position, singleCharacters: true } );
// We will count visited characters and return the position after `howMany` characters.
let charactersFound = 0;
for ( const value of walker ) {
if ( value.type == 'text' ) {
charactersFound++;
if ( charactersFound == howMany ) {
return walker.position;
}
}
}
}
/**
* Creates `span` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from information
* provided by {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor} object. If priority
* is not provided in descriptor - default priority will be used.
*
* @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} descriptor
* @return {module:engine/conversion/model-to-view-converters~HighlightAttributeElement}
*/
function createViewElementFromHighlightDescriptor( descriptor ) {
const viewElement = new HighlightAttributeElement( 'span', descriptor.attributes );
if ( descriptor.class ) {
const cssClasses = Array.isArray( descriptor.class ) ? descriptor.class : [ descriptor.class ];
viewElement.addClass( ...cssClasses );
}
if ( descriptor.priority ) {
viewElement.priority = descriptor.priority;
}
viewElement.setCustomProperty( 'highlightDescriptorId', descriptor.id );
return viewElement;
}
/**
* Special kind of {@link module:engine/view/attributeelement~AttributeElement} that is created and used in
* marker-to-highlight conversion.
*
* The difference between `HighlightAttributeElement` and {@link module:engine/view/attributeelement~AttributeElement}
* is {@link module:engine/view/attributeelement~AttributeElement#isSimilar} method.
*
* For `HighlightAttributeElement` it checks just `highlightDescriptorId` custom property, that is set during marker-to-highlight
* conversion basing on {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor} object.
* `HighlightAttributeElement`s with same `highlightDescriptorId` property are considered similar.
*/
class HighlightAttributeElement extends AttributeElement {
isSimilar( otherElement ) {
if ( otherElement.is( 'attributeElement' ) ) {
return this.getCustomProperty( 'highlightDescriptorId' ) === otherElement.getCustomProperty( 'highlightDescriptorId' );
}
return false;
}
}
/**
* Object describing how the content highlight should be created in the view.
*
* Each text node contained in the highlight will be wrapped with `span` element with CSS class(es), attributes and priority
* described by this object.
*
* Each element can handle displaying the highlight separately by providing `addHighlight` and `removeHighlight` custom
* properties:
* * `HighlightDescriptor` is passed to the `addHighlight` function upon conversion and should be used to apply the highlight to
* the element,
* * descriptor id is passed to the `removeHighlight` function upon conversion and should be used to remove the highlight of given
* id from the element.
*
* @typedef {Object} module:engine/conversion/model-to-view-converters~HighlightDescriptor
*
* @property {String|Array.<String>} class CSS class or array of classes to set. If descriptor is used to
* create {@link module:engine/view/attributeelement~AttributeElement} over text nodes, those classes will be set
* on that {@link module:engine/view/attributeelement~AttributeElement}. If descriptor is applied to an element,
* usually those class will be set on that element, however this depends on how the element converts the descriptor.
*
* @property {String} [id] Descriptor identifier. If not provided, defaults to converted marker's name.
*
* @property {Number} [priority] Descriptor priority. If not provided, defaults to `10`. If descriptor is used to create
* {@link module:engine/view/attributeelement~AttributeElement}, it will be that element's
* {@link module:engine/view/attributeelement~AttributeElement#priority}. If descriptor is applied to an element,
* the priority will be used to determine which descriptor is more important.
*
* @property {Object} [attributes] Attributes to set. If descriptor is used to create
* {@link module:engine/view/attributeelement~AttributeElement} over text nodes, those attributes will be set on that
* {@link module:engine/view/attributeelement~AttributeElement}. If descriptor is applied to an element, usually those
* attributes will be set on that element, however this depends on how the element converts the descriptor.
*/
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/conversion/viewconsumable.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/conversion/viewconsumable
*/
/**
* Class used for handling consumption of view {@link module:engine/view/element~Element elements},
* {@link module:engine/view/text~Text text nodes} and {@link module:engine/view/documentfragment~DocumentFragment document fragments}.
* Element's name and its parts (attributes, classes and styles) can be consumed separately. Consuming an element's name
* does not consume its attributes, classes and styles.
* To add items for consumption use {@link module:engine/conversion/viewconsumable~ViewConsumable#add add method}.
* To test items use {@link module:engine/conversion/viewconsumable~ViewConsumable#test test method}.
* To consume items use {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consume method}.
* To revert already consumed items use {@link module:engine/conversion/viewconsumable~ViewConsumable#revert revert method}.
*
* viewConsumable.add( element, { name: true } ); // Adds element's name as ready to be consumed.
* viewConsumable.add( textNode ); // Adds text node for consumption.
* viewConsumable.add( docFragment ); // Adds document fragment for consumption.
* viewConsumable.test( element, { name: true } ); // Tests if element's name can be consumed.
* viewConsumable.test( textNode ); // Tests if text node can be consumed.
* viewConsumable.test( docFragment ); // Tests if document fragment can be consumed.
* viewConsumable.consume( element, { name: true } ); // Consume element's name.
* viewConsumable.consume( textNode ); // Consume text node.
* viewConsumable.consume( docFragment ); // Consume document fragment.
* viewConsumable.revert( element, { name: true } ); // Revert already consumed element's name.
* viewConsumable.revert( textNode ); // Revert already consumed text node.
* viewConsumable.revert( docFragment ); // Revert already consumed document fragment.
*/
class ViewConsumable {
/**
* Creates new ViewConsumable.
*/
constructor() {
/**
* Map of consumable elements. If {@link module:engine/view/element~Element element} is used as a key,
* {@link module:engine/conversion/viewconsumable~ViewElementConsumables ViewElementConsumables} instance is stored as value.
* For {@link module:engine/view/text~Text text nodes} and
* {@link module:engine/view/documentfragment~DocumentFragment document fragments} boolean value is stored as value.
*
* @protected
* @member {Map.<module:engine/conversion/viewconsumable~ViewElementConsumables|Boolean>}
*/
this._consumables = new Map();
}
/**
* Adds {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
* {@link module:engine/view/documentfragment~DocumentFragment document fragment} as ready to be consumed.
*
* viewConsumable.add( p, { name: true } ); // Adds element's name to consume.
* viewConsumable.add( p, { attribute: 'name' } ); // Adds element's attribute.
* viewConsumable.add( p, { class: 'foobar' } ); // Adds element's class.
* viewConsumable.add( p, { style: 'color' } ); // Adds element's style
* viewConsumable.add( p, { attribute: 'name', style: 'color' } ); // Adds attribute and style.
* viewConsumable.add( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be provided.
* viewConsumable.add( textNode ); // Adds text node to consume.
* viewConsumable.add( docFragment ); // Adds document fragment to consume.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
* attribute is provided - it should be handled separately by providing actual style/class.
*
* viewConsumable.add( p, { attribute: 'style' } ); // This call will throw an exception.
* viewConsumable.add( p, { style: 'color' } ); // This is properly handled style.
*
* @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
* @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
* @param {Boolean} consumables.name If set to true element's name will be included.
* @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
* @param {String|Array.<String>} consumables.class Class name or array of class names.
* @param {String|Array.<String>} consumables.style Style name or array of style names.
*/
add( element, consumables ) {
let elementConsumables;
// For text nodes and document fragments just mark them as consumable.
if ( element.is( 'text' ) || element.is( 'documentFragment' ) ) {
this._consumables.set( element, true );
return;
}
// For elements create new ViewElementConsumables or update already existing one.
if ( !this._consumables.has( element ) ) {
elementConsumables = new viewconsumable_ViewElementConsumables();
this._consumables.set( element, elementConsumables );
} else {
elementConsumables = this._consumables.get( element );
}
elementConsumables.add( consumables );
}
/**
* Tests if {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
* {@link module:engine/view/documentfragment~DocumentFragment document fragment} can be consumed.
* It returns `true` when all items included in method's call can be consumed. Returns `false` when
* first already consumed item is found and `null` when first non-consumable item is found.
*
* viewConsumable.test( p, { name: true } ); // Tests element's name.
* viewConsumable.test( p, { attribute: 'name' } ); // Tests attribute.
* viewConsumable.test( p, { class: 'foobar' } ); // Tests class.
* viewConsumable.test( p, { style: 'color' } ); // Tests style.
* viewConsumable.test( p, { attribute: 'name', style: 'color' } ); // Tests attribute and style.
* viewConsumable.test( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be tested.
* viewConsumable.test( textNode ); // Tests text node.
* viewConsumable.test( docFragment ); // Tests document fragment.
*
* Testing classes and styles as attribute will test if all added classes/styles can be consumed.
*
* viewConsumable.test( p, { attribute: 'class' } ); // Tests if all added classes can be consumed.
* viewConsumable.test( p, { attribute: 'style' } ); // Tests if all added styles can be consumed.
*
* @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
* @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
* @param {Boolean} consumables.name If set to true element's name will be included.
* @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
* @param {String|Array.<String>} consumables.class Class name or array of class names.
* @param {String|Array.<String>} consumables.style Style name or array of style names.
* @returns {Boolean|null} Returns `true` when all items included in method's call can be consumed. Returns `false`
* when first already consumed item is found and `null` when first non-consumable item is found.
*/
test( element, consumables ) {
const elementConsumables = this._consumables.get( element );
if ( elementConsumables === undefined ) {
return null;
}
// For text nodes and document fragments return stored boolean value.
if ( element.is( 'text' ) || element.is( 'documentFragment' ) ) {
return elementConsumables;
}
// For elements test consumables object.
return elementConsumables.test( consumables );
}
/**
* Consumes {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
* {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
* It returns `true` when all items included in method's call can be consumed, otherwise returns `false`.
*
* viewConsumable.consume( p, { name: true } ); // Consumes element's name.
* viewConsumable.consume( p, { attribute: 'name' } ); // Consumes element's attribute.
* viewConsumable.consume( p, { class: 'foobar' } ); // Consumes element's class.
* viewConsumable.consume( p, { style: 'color' } ); // Consumes element's style.
* viewConsumable.consume( p, { attribute: 'name', style: 'color' } ); // Consumes attribute and style.
* viewConsumable.consume( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be consumed.
* viewConsumable.consume( textNode ); // Consumes text node.
* viewConsumable.consume( docFragment ); // Consumes document fragment.
*
* Consuming classes and styles as attribute will test if all added classes/styles can be consumed.
*
* viewConsumable.consume( p, { attribute: 'class' } ); // Consume only if all added classes can be consumed.
* viewConsumable.consume( p, { attribute: 'style' } ); // Consume only if all added styles can be consumed.
*
* @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
* @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
* @param {Boolean} consumables.name If set to true element's name will be included.
* @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
* @param {String|Array.<String>} consumables.class Class name or array of class names.
* @param {String|Array.<String>} consumables.style Style name or array of style names.
* @returns {Boolean} Returns `true` when all items included in method's call can be consumed,
* otherwise returns `false`.
*/
consume( element, consumables ) {
if ( this.test( element, consumables ) ) {
if ( element.is( 'text' ) || element.is( 'documentFragment' ) ) {
// For text nodes and document fragments set value to false.
this._consumables.set( element, false );
} else {
// For elements - consume consumables object.
this._consumables.get( element ).consume( consumables );
}
return true;
}
return false;
}
/**
* Reverts {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
* {@link module:engine/view/documentfragment~DocumentFragment document fragment} so they can be consumed once again.
* Method does not revert items that were never previously added for consumption, even if they are included in
* method's call.
*
* viewConsumable.revert( p, { name: true } ); // Reverts element's name.
* viewConsumable.revert( p, { attribute: 'name' } ); // Reverts element's attribute.
* viewConsumable.revert( p, { class: 'foobar' } ); // Reverts element's class.
* viewConsumable.revert( p, { style: 'color' } ); // Reverts element's style.
* viewConsumable.revert( p, { attribute: 'name', style: 'color' } ); // Reverts attribute and style.
* viewConsumable.revert( p, { class: [ 'baz', 'bar' ] } ); // Multiple names can be reverted.
* viewConsumable.revert( textNode ); // Reverts text node.
* viewConsumable.revert( docFragment ); // Reverts document fragment.
*
* Reverting classes and styles as attribute will revert all classes/styles that were previously added for
* consumption.
*
* viewConsumable.revert( p, { attribute: 'class' } ); // Reverts all classes added for consumption.
* viewConsumable.revert( p, { attribute: 'style' } ); // Reverts all styles added for consumption.
*
* @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
* @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
* @param {Boolean} consumables.name If set to true element's name will be included.
* @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
* @param {String|Array.<String>} consumables.class Class name or array of class names.
* @param {String|Array.<String>} consumables.style Style name or array of style names.
*/
revert( element, consumables ) {
const elementConsumables = this._consumables.get( element );
if ( elementConsumables !== undefined ) {
if ( element.is( 'text' ) || element.is( 'documentFragment' ) ) {
// For text nodes and document fragments - set consumable to true.
this._consumables.set( element, true );
} else {
// For elements - revert items from consumables object.
elementConsumables.revert( consumables );
}
}
}
/**
* Creates consumable object from {@link module:engine/view/element~Element view element}. Consumable object will include
* element's name and all its attributes, classes and styles.
*
* @static
* @param {module:engine/view/element~Element} element
* @returns {Object} consumables
*/
static consumablesFromElement( element ) {
const consumables = {
name: true,
attribute: [],
class: [],
style: []
};
const attributes = element.getAttributeKeys();
for ( const attribute of attributes ) {
// Skip classes and styles - will be added separately.
if ( attribute == 'style' || attribute == 'class' ) {
continue;
}
consumables.attribute.push( attribute );
}
const classes = element.getClassNames();
for ( const className of classes ) {
consumables.class.push( className );
}
const styles = element.getStyleNames();
for ( const style of styles ) {
consumables.style.push( style );
}
return consumables;
}
/**
* Creates {@link module:engine/conversion/viewconsumable~ViewConsumable ViewConsumable} instance from
* {@link module:engine/view/node~Node node} or {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
* Instance will contain all elements, child nodes, attributes, styles and classes added for consumption.
*
* @static
* @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} from View node or document fragment
* from which `ViewConsumable` will be created.
* @param {module:engine/conversion/viewconsumable~ViewConsumable} [instance] If provided, given `ViewConsumable` instance will be used
* to add all consumables. It will be returned instead of a new instance.
*/
static createFrom( from, instance ) {
if ( !instance ) {
instance = new ViewConsumable();
}
if ( from.is( 'text' ) ) {
instance.add( from );
return instance;
}
// Add `from` itself, if it is an element.
if ( from.is( 'element' ) ) {
instance.add( from, ViewConsumable.consumablesFromElement( from ) );
}
if ( from.is( 'documentFragment' ) ) {
instance.add( from );
}
for ( const child of from.getChildren() ) {
instance = ViewConsumable.createFrom( child, instance );
}
return instance;
}
}
/**
* This is a private helper-class for {@link module:engine/conversion/viewconsumable~ViewConsumable}.
* It represents and manipulates consumable parts of a single {@link module:engine/view/element~Element}.
*
* @private
*/
class viewconsumable_ViewElementConsumables {
/**
* Creates ViewElementConsumables instance.
*/
constructor() {
/**
* Flag indicating if name of the element can be consumed.
*
* @private
* @member {Boolean}
*/
this._canConsumeName = null;
/**
* Contains maps of element's consumables: attributes, classes and styles.
*
* @private
* @member {Object}
*/
this._consumables = {
attribute: new Map(),
style: new Map(),
class: new Map()
};
}
/**
* Adds consumable parts of the {@link module:engine/view/element~Element view element}.
* Element's name itself can be marked to be consumed (when element's name is consumed its attributes, classes and
* styles still could be consumed):
*
* consumables.add( { name: true } );
*
* Attributes classes and styles:
*
* consumables.add( { attribute: 'title', class: 'foo', style: 'color' } );
* consumables.add( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
* attribute is provided - it should be handled separately by providing `style` and `class` in consumables object.
*
* @param {Object} consumables Object describing which parts of the element can be consumed.
* @param {Boolean} consumables.name If set to `true` element's name will be added as consumable.
* @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to add as consumable.
* @param {String|Array.<String>} consumables.class Class name or array of class names to add as consumable.
* @param {String|Array.<String>} consumables.style Style name or array of style names to add as consumable.
*/
add( consumables ) {
if ( consumables.name ) {
this._canConsumeName = true;
}
for ( const type in this._consumables ) {
if ( type in consumables ) {
this._add( type, consumables[ type ] );
}
}
}
/**
* Tests if parts of the {@link module:engine/view/node~Node view node} can be consumed.
*
* Element's name can be tested:
*
* consumables.test( { name: true } );
*
* Attributes classes and styles:
*
* consumables.test( { attribute: 'title', class: 'foo', style: 'color' } );
* consumables.test( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
*
* @param {Object} consumables Object describing which parts of the element should be tested.
* @param {Boolean} consumables.name If set to `true` element's name will be tested.
* @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to test.
* @param {String|Array.<String>} consumables.class Class name or array of class names to test.
* @param {String|Array.<String>} consumables.style Style name or array of style names to test.
* @returns {Boolean|null} `true` when all tested items can be consumed, `null` when even one of the items
* was never marked for consumption and `false` when even one of the items was already consumed.
*/
test( consumables ) {
// Check if name can be consumed.
if ( consumables.name && !this._canConsumeName ) {
return this._canConsumeName;
}
for ( const type in this._consumables ) {
if ( type in consumables ) {
const value = this._test( type, consumables[ type ] );
if ( value !== true ) {
return value;
}
}
}
// Return true only if all can be consumed.
return true;
}
/**
* Consumes parts of {@link module:engine/view/element~Element view element}. This function does not check if consumable item
* is already consumed - it consumes all consumable items provided.
* Element's name can be consumed:
*
* consumables.consume( { name: true } );
*
* Attributes classes and styles:
*
* consumables.consume( { attribute: 'title', class: 'foo', style: 'color' } );
* consumables.consume( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
*
* @param {Object} consumables Object describing which parts of the element should be consumed.
* @param {Boolean} consumables.name If set to `true` element's name will be consumed.
* @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to consume.
* @param {String|Array.<String>} consumables.class Class name or array of class names to consume.
* @param {String|Array.<String>} consumables.style Style name or array of style names to consume.
*/
consume( consumables ) {
if ( consumables.name ) {
this._canConsumeName = false;
}
for ( const type in this._consumables ) {
if ( type in consumables ) {
this._consume( type, consumables[ type ] );
}
}
}
/**
* Revert already consumed parts of {@link module:engine/view/element~Element view Element}, so they can be consumed once again.
* Element's name can be reverted:
*
* consumables.revert( { name: true } );
*
* Attributes classes and styles:
*
* consumables.revert( { attribute: 'title', class: 'foo', style: 'color' } );
* consumables.revert( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
*
* @param {Object} consumables Object describing which parts of the element should be reverted.
* @param {Boolean} consumables.name If set to `true` element's name will be reverted.
* @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to revert.
* @param {String|Array.<String>} consumables.class Class name or array of class names to revert.
* @param {String|Array.<String>} consumables.style Style name or array of style names to revert.
*/
revert( consumables ) {
if ( consumables.name ) {
this._canConsumeName = true;
}
for ( const type in this._consumables ) {
if ( type in consumables ) {
this._revert( type, consumables[ type ] );
}
}
}
/**
* Helper method that adds consumables of a given type: attribute, class or style.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
* type is provided - it should be handled separately by providing actual style/class type.
*
* @private
* @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
* @param {String|Array.<String>} item Consumable item or array of items.
*/
_add( type, item ) {
const items = lodash_isArray( item ) ? item : [ item ];
const consumables = this._consumables[ type ];
for ( const name of items ) {
if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
/**
* Class and style attributes should be handled separately in
* {@link module:engine/conversion/viewconsumable~ViewConsumable#add `ViewConsumable#add()`}.
*
* What you have done is trying to use:
*
* consumables.add( { attribute: [ 'class', 'style' ] } );
*
* While each class and style should be registered separately:
*
* consumables.add( { class: 'some-class', style: 'font-weight' } );
*
* @error viewconsumable-invalid-attribute
*/
throw new CKEditorError( 'viewconsumable-invalid-attribute: Classes and styles should be handled separately.' );
}
consumables.set( name, true );
}
}
/**
* Helper method that tests consumables of a given type: attribute, class or style.
*
* @private
* @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
* @param {String|Array.<String>} item Consumable item or array of items.
* @returns {Boolean|null} Returns `true` if all items can be consumed, `null` when one of the items cannot be
* consumed and `false` when one of the items is already consumed.
*/
_test( type, item ) {
const items = lodash_isArray( item ) ? item : [ item ];
const consumables = this._consumables[ type ];
for ( const name of items ) {
if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
// Check all classes/styles if class/style attribute is tested.
const value = this._test( name, [ ...this._consumables[ name ].keys() ] );
if ( value !== true ) {
return value;
}
} else {
const value = consumables.get( name );
// Return null if attribute is not found.
if ( value === undefined ) {
return null;
}
if ( !value ) {
return false;
}
}
}
return true;
}
/**
* Helper method that consumes items of a given type: attribute, class or style.
*
* @private
* @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
* @param {String|Array.<String>} item Consumable item or array of items.
*/
_consume( type, item ) {
const items = lodash_isArray( item ) ? item : [ item ];
const consumables = this._consumables[ type ];
for ( const name of items ) {
if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
// If class or style is provided for consumption - consume them all.
this._consume( name, [ ...this._consumables[ name ].keys() ] );
} else {
consumables.set( name, false );
}
}
}
/**
* Helper method that reverts items of a given type: attribute, class or style.
*
* @private
* @param {String} type Type of the consumable item: `attribute`, `class` or , `style`.
* @param {String|Array.<String>} item Consumable item or array of items.
*/
_revert( type, item ) {
const items = lodash_isArray( item ) ? item : [ item ];
const consumables = this._consumables[ type ];
for ( const name of items ) {
if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
// If class or style is provided for reverting - revert them all.
this._revert( name, [ ...this._consumables[ name ].keys() ] );
} else {
const value = consumables.get( name );
if ( value === false ) {
consumables.set( name, true );
}
}
}
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/writer.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/writer
*/
/**
* Contains functions used for composing model tree, grouped together under "model writer" name. Those functions
* are built on top of {@link module:engine/model/node~Node node}, and it's child classes', APIs.
*
* Model writer API has multiple advantages and it is highly recommended to use it when changing model tree and nodes:
* * model writer API {@link module:engine/model/writer~writer.normalizeNodes normalizes inserted nodes}, which means that you can insert
* not only {@link module:engine/model/node~Node nodes}, but also `String`s, {@link module:engine/model/textproxy~TextProxy text proxies}
* and
* {@link module:engine/model/documentfragment~DocumentFragment document fragments},
* * model writer API operates on {@link module:engine/model/position~Position positions}, which means that you have
* better control over manipulating model tree as positions operate on offsets rather than indexes,
* * model writer API automatically merges {@link module:engine/model/text~Text text nodes} with same attributes, which means
* lower memory usage and better efficiency.
*
* @namespace writer
*/
const writer_writer = {
insert: model_writer_insert,
remove: model_writer_remove,
move: model_writer_move,
setAttribute: writer_setAttribute,
removeAttribute: writer_removeAttribute,
normalizeNodes
};
/* harmony default export */ var model_writer = (writer_writer);
/**
* Inserts given nodes at given position.
*
* @function module:engine/model/writer~writer.insert
* @param {module:engine/model/position~Position} position Position at which nodes should be inserted.
* @param {module:engine/model/node~NodeSet} nodes Nodes to insert.
* @returns {module:engine/model/range~Range} Range spanning over inserted elements.
*/
function model_writer_insert( position, nodes ) {
nodes = normalizeNodes( nodes );
// We have to count offset before inserting nodes because they can get merged and we would get wrong offsets.
const offset = nodes.reduce( ( sum, node ) => sum + node.offsetSize, 0 );
const parent = position.parent;
// Insertion might be in a text node, we should split it if that's the case.
_splitNodeAtPosition( position );
const index = position.index;
// Insert nodes at given index. After splitting we have a proper index and insertion is between nodes,
// using basic `Element` API.
parent.insertChildren( index, nodes );
// Merge text nodes, if possible. Merging is needed only at points where inserted nodes "touch" "old" nodes.
_mergeNodesAtIndex( parent, index + nodes.length );
_mergeNodesAtIndex( parent, index );
return new range_Range( position, position.getShiftedBy( offset ) );
}
/**
* Removed nodes in given range. Only {@link module:engine/model/range~Range#isFlat flat} ranges are accepted.
*
* @function module:engine/model/writer~writer.remove
* @param {module:engine/model/range~Range} range Range containing nodes to remove.
* @returns {Array.<module:engine/model/node~Node>}
*/
function model_writer_remove( range ) {
if ( !range.isFlat ) {
/**
* Trying to remove a range which starts and ends in different element.
*
* @error model-writer-remove-range-not-flat
*/
throw new CKEditorError( 'model-writer-remove-range-not-flat: ' +
'Trying to remove a range which starts and ends in different element.' );
}
const parent = range.start.parent;
// Range may be inside text nodes, we have to split them if that's the case.
_splitNodeAtPosition( range.start );
_splitNodeAtPosition( range.end );
// Remove the text nodes using basic `Element` API.
const removed = parent.removeChildren( range.start.index, range.end.index - range.start.index );
// Merge text nodes, if possible. After some nodes were removed, node before and after removed range will be
// touching at the position equal to the removed range beginning. We check merging possibility there.
_mergeNodesAtIndex( parent, range.start.index );
return removed;
}
/**
* Moves nodes in given range to given target position. Only {@link module:engine/model/range~Range#isFlat flat} ranges are accepted.
*
* @param {module:engine/model/range~Range} sourceRange Range containing nodes to move.
* @param {module:engine/model/position~Position} targetPosition Position to which nodes should be moved.
* @returns {module:engine/model/range~Range} Range containing moved nodes.
*/
function model_writer_move( sourceRange, targetPosition ) {
if ( !sourceRange.isFlat ) {
/**
* Trying to move a range which starts and ends in different element.
*
* @error model-writer-move-range-not-flat
*/
throw new CKEditorError( 'model-writer-move-range-not-flat: ' +
'Trying to move a range which starts and ends in different element.' );
}
const nodes = this.remove( sourceRange );
// We have to fix `targetPosition` because model changed after nodes from `sourceRange` got removed and
// that change might have an impact on `targetPosition`.
targetPosition = targetPosition._getTransformedByDeletion( sourceRange.start, sourceRange.end.offset - sourceRange.start.offset );
return this.insert( targetPosition, nodes );
}
/**
* Sets given attribute on nodes in given range.
*
* @param {module:engine/model/range~Range} range Range containing nodes that should have the attribute set.
* @param {String} key Key of attribute to set.
* @param {*} value Attribute value.
*/
function writer_setAttribute( range, key, value ) {
// Range might start or end in text nodes, so we have to split them.
_splitNodeAtPosition( range.start );
_splitNodeAtPosition( range.end );
// Iterate over all items in the range.
for ( const item of range.getItems() ) {
// Iterator will return `TextProxy` instances but we know that those text proxies will
// always represent full text nodes (this is guaranteed thanks to splitting we did before).
// So, we can operate on those text proxies' text nodes.
const node = item.is( 'textProxy' ) ? item.textNode : item;
if ( value !== null ) {
node.setAttribute( key, value );
} else {
node.removeAttribute( key );
}
// After attributes changing it may happen that some text nodes can be merged. Try to merge with previous node.
_mergeNodesAtIndex( node.parent, node.index );
}
// Try to merge last changed node with it's previous sibling (not covered by the loop above).
_mergeNodesAtIndex( range.end.parent, range.end.index );
}
/**
* Removes given attribute from nodes in given range.
*
* @param {module:engine/model/range~Range} range Range containing nodes that should have the attribute removed.
* @param {String} key Key of attribute to remove.
*/
function writer_removeAttribute( range, key ) {
this.setAttribute( range, key, null );
}
/**
* Normalizes given object or an array of objects to an array of {@link module:engine/model/node~Node nodes}. See
* {@link module:engine/model/node~NodeSet NodeSet} for details on how normalization is performed.
*
* @param {module:engine/model/node~NodeSet} nodes Objects to normalize.
* @returns {Array.<module:engine/model/node~Node>} Normalized nodes.
*/
function normalizeNodes( nodes ) {
const normalized = [];
if ( !( nodes instanceof Array ) ) {
nodes = [ nodes ];
}
// Convert instances of classes other than Node.
for ( let i = 0; i < nodes.length; i++ ) {
if ( typeof nodes[ i ] == 'string' ) {
normalized.push( new text_Text( nodes[ i ] ) );
} else if ( nodes[ i ] instanceof textproxy_TextProxy ) {
normalized.push( new text_Text( nodes[ i ].data, nodes[ i ].getAttributes() ) );
} else if ( nodes[ i ] instanceof documentfragment_DocumentFragment || nodes[ i ] instanceof nodelist_NodeList ) {
for ( const child of nodes[ i ] ) {
normalized.push( child );
}
} else if ( nodes[ i ] instanceof node_Node ) {
normalized.push( nodes[ i ] );
}
// Skip unrecognized type.
}
// Merge text nodes.
for ( let i = 1; i < normalized.length; i++ ) {
const node = normalized[ i ];
const prev = normalized[ i - 1 ];
if ( node instanceof text_Text && prev instanceof text_Text && _haveSameAttributes( node, prev ) ) {
// Doing this instead changing prev.data because .data is readonly.
normalized.splice( i - 1, 2, new text_Text( prev.data + node.data, prev.getAttributes() ) );
i--;
}
}
return normalized;
}
/**
* Checks if nodes before and after given index in given element are {@link module:engine/model/text~Text text nodes} and
* merges them into one node if they have same attributes.
*
* Merging is done by removing two text nodes and inserting a new text node containing data from both merged text nodes.
*
* @ignore
* @private
* @param {module:engine/model/element~Element} element Parent element of nodes to merge.
* @param {Number} index Index between nodes to merge.
*/
function _mergeNodesAtIndex( element, index ) {
const nodeBefore = element.getChild( index - 1 );
const nodeAfter = element.getChild( index );
// Check if both of those nodes are text objects with same attributes.
if ( nodeBefore && nodeAfter && nodeBefore.is( 'text' ) && nodeAfter.is( 'text' ) && _haveSameAttributes( nodeBefore, nodeAfter ) ) {
// Append text of text node after index to the before one.
const mergedNode = new text_Text( nodeBefore.data + nodeAfter.data, nodeBefore.getAttributes() );
// Remove separate text nodes.
element.removeChildren( index - 1, 2 );
// Insert merged text node.
element.insertChildren( index - 1, mergedNode );
}
}
/**
* Checks if given position is in a text node, and if so, splits the text node in two text nodes, each of them
* containing a part of original text node.
*
* @ignore
* @private
* @param {module:engine/model/position~Position} position Position at which node should be split.
*/
function _splitNodeAtPosition( position ) {
const textNode = position.textNode;
const element = position.parent;
if ( textNode ) {
const offsetDiff = position.offset - textNode.startOffset;
const index = textNode.index;
element.removeChildren( index, 1 );
const firstPart = new text_Text( textNode.data.substr( 0, offsetDiff ), textNode.getAttributes() );
const secondPart = new text_Text( textNode.data.substr( offsetDiff ), textNode.getAttributes() );
element.insertChildren( index, [ firstPart, secondPart ] );
}
}
/**
* Checks whether two given nodes have same attributes.
*
* @ignore
* @private
* @param {module:engine/model/node~Node} nodeA Node to check.
* @param {module:engine/model/node~Node} nodeB Node to check.
* @returns {Boolean} `true` if nodes have same attributes, `false` otherwise.
*/
function _haveSameAttributes( nodeA, nodeB ) {
const iteratorA = nodeA.getAttributes();
const iteratorB = nodeB.getAttributes();
for ( const attr of iteratorA ) {
if ( attr[ 1 ] !== nodeB.getAttribute( attr[ 0 ] ) ) {
return false;
}
iteratorB.next();
}
return iteratorB.next().done;
}
/**
* Value that can be normalized to an array of {@link module:engine/model/node~Node nodes}.
*
* Non-arrays are normalized as follows:
* * {@link module:engine/model/node~Node Node} is left as is,
* * {@link module:engine/model/textproxy~TextProxy TextProxy} and `String` are normalized to {@link module:engine/model/text~Text Text},
* * {@link module:engine/model/nodelist~NodeList NodeList} is normalized to an array containing all nodes that are in that node list,
* * {@link module:engine/model/documentfragment~DocumentFragment DocumentFragment} is normalized to an array containing all of it's
* * children.
*
* Arrays are processed item by item like non-array values and flattened to one array. Normalization always results in
* a flat array of {@link module:engine/model/node~Node nodes}. Consecutive text nodes (or items normalized to text nodes) will be
* merged if they have same attributes.
*
* @typedef {module:engine/model/node~Node|module:engine/model/textproxy~TextProxy|String|
* module:engine/model/nodelist~NodeList|module:engine/model/documentfragment~DocumentFragment|Iterable}
* module:engine/model/node~NodeSet
*/
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/conversion/viewconversiondispatcher.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/conversion/viewconversiondispatcher
*/
/**
* `ViewConversionDispatcher` is a central point of {@link module:engine/view/view view} conversion, which is a process of
* converting given {@link module:engine/view/documentfragment~DocumentFragment view document fragment} or
* {@link module:engine/view/element~Element}
* into another structure. In default application, {@link module:engine/view/view view} is converted to {@link module:engine/model/model}.
*
* During conversion process, for all {@link module:engine/view/node~Node view nodes} from the converted view document fragment,
* `ViewConversionDispatcher` fires corresponding events. Special callbacks called "converters" should listen to
* `ViewConversionDispatcher` for those events.
*
* Each callback, as a first argument, is passed a special object `data` that has `input` and `output` properties.
* `input` property contains {@link module:engine/view/node~Node view node} or
* {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
* that is converted at the moment and might be handled by the callback. `output` property should be used to save the result
* of conversion. Keep in mind that the `data` parameter is customizable and may contain other values - see
* {@link ~ViewConversionDispatcher#convert}. It is also shared by reference by all callbacks
* listening to given event. **Note**: in view to model conversion - `data` contains `context` property that is an array
* of {@link module:engine/model/element~Element model elements}. These are model elements that will be the parent of currently
* converted view item. `context` property is used in examples below.
*
* The second parameter passed to a callback is an instance of {@link module:engine/conversion/viewconsumable~ViewConsumable}. It stores
* information about what parts of processed view item are still waiting to be handled. After a piece of view item
* was converted, appropriate consumable value should be {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consumed}.
*
* The third parameter passed to a callback is an instance of {@link ~ViewConversionDispatcher}
* which provides additional tools for converters.
*
* Examples of providing callbacks for `ViewConversionDispatcher`:
*
* // Converter for paragraphs (<p>).
* viewDispatcher.on( 'element:p', ( evt, data, consumable, conversionApi ) => {
* const paragraph = new ModelElement( 'paragraph' );
* const schemaQuery = {
* name: 'paragraph',
* inside: data.context
* };
*
* if ( conversionApi.schema.check( schemaQuery ) ) {
* if ( !consumable.consume( data.input, { name: true } ) ) {
* // Before converting this paragraph's children we have to update their context by this paragraph.
* data.context.push( paragraph );
* const children = conversionApi.convertChildren( data.input, consumable, data );
* data.context.pop();
* paragraph.appendChildren( children );
* data.output = paragraph;
* }
* }
* } );
*
* // Converter for links (<a>).
* viewDispatcher.on( 'element:a', ( evt, data, consumable, conversionApi ) => {
* if ( consumable.consume( data.input, { name: true, attributes: [ 'href' ] } ) ) {
* // <a> element is inline and is represented by an attribute in the model.
* // This is why we are not updating `context` property.
* data.output = conversionApi.convertChildren( data.input, consumable, data );
*
* for ( let item of Range.createFrom( data.output ) ) {
* const schemaQuery = {
* name: item.name || '$text',
* attribute: 'link',
* inside: data.context
* };
*
* if ( conversionApi.schema.check( schemaQuery ) ) {
* item.setAttribute( 'link', data.input.getAttribute( 'href' ) );
* }
* }
* }
* } );
*
* // Fire conversion.
* // Always take care where the converted model structure will be appended to. If this `viewDocumentFragment`
* // is going to be appended directly to a '$root' element, use that in `context`.
* viewDispatcher.convert( viewDocumentFragment, { context: [ '$root' ] } );
*
* Before each conversion process, `ViewConversionDispatcher` fires {@link ~ViewConversionDispatcher#event:viewCleanup}
* event which can be used to prepare tree view for conversion.
*
* @mixes module:utils/emittermixin~EmitterMixin
* @fires viewCleanup
* @fires element
* @fires text
* @fires documentFragment
*/
class viewconversiondispatcher_ViewConversionDispatcher {
/**
* Creates a `ViewConversionDispatcher` that operates using passed API.
*
* @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi
* @param {Object} [conversionApi] Additional properties for interface that will be passed to events fired
* by `ViewConversionDispatcher`.
*/
constructor( conversionApi = {} ) {
/**
* Interface passed by dispatcher to the events callbacks.
*
* @member {module:engine/conversion/viewconversiondispatcher~ViewConversionApi}
*/
this.conversionApi = lodash_assignIn( {}, conversionApi );
// `convertItem` and `convertChildren` are bound to this `ViewConversionDispatcher` instance and
// set on `conversionApi`. This way only a part of `ViewConversionDispatcher` API is exposed.
this.conversionApi.convertItem = this._convertItem.bind( this );
this.conversionApi.convertChildren = this._convertChildren.bind( this );
}
/**
* Starts the conversion process. The entry point for the conversion.
*
* @fires element
* @fires text
* @fires documentFragment
* @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} viewItem
* Part of the view to be converted.
* @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
* events. See also {@link ~ViewConversionDispatcher#event:element element event}.
* @returns {module:engine/model/documentfragment~DocumentFragment} Model data that is a result of the conversion process
* wrapped in `DocumentFragment`. Converted marker elements will be set as that document fragment's
* {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
*/
convert( viewItem, additionalData = {} ) {
this.fire( 'viewCleanup', viewItem );
const consumable = ViewConsumable.createFrom( viewItem );
let conversionResult = this._convertItem( viewItem, consumable, additionalData );
// We can get a null here if conversion failed (see _convertItem())
// or simply if an item could not be converted (e.g. due to the schema).
if ( !conversionResult ) {
return new documentfragment_DocumentFragment();
}
// When conversion result is not a document fragment we need to wrap it in document fragment.
if ( !conversionResult.is( 'documentFragment' ) ) {
conversionResult = new documentfragment_DocumentFragment( [ conversionResult ] );
}
// Extract temporary markers elements from model and set as static markers collection.
conversionResult.markers = extractMarkersFromModelFragment( conversionResult );
return conversionResult;
}
/**
* @private
* @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertItem
*/
_convertItem( input, consumable, additionalData = {} ) {
const data = lodash_assignIn( {}, additionalData, {
input,
output: null
} );
if ( input.is( 'element' ) ) {
this.fire( 'element:' + input.name, data, consumable, this.conversionApi );
} else if ( input.is( 'text' ) ) {
this.fire( 'text', data, consumable, this.conversionApi );
} else {
this.fire( 'documentFragment', data, consumable, this.conversionApi );
}
// Handle incorrect `data.output`.
if ( data.output && !( data.output instanceof node_Node || data.output instanceof documentfragment_DocumentFragment ) ) {
/**
* Incorrect conversion result was dropped.
*
* Item may be converted to either {@link module:engine/model/node~Node model node} or
* {@link module:engine/model/documentfragment~DocumentFragment model document fragment}.
*
* @error view-conversion-dispatcher-incorrect-result
*/
src_log.warn( 'view-conversion-dispatcher-incorrect-result: Incorrect conversion result was dropped.', [ input, data.output ] );
return null;
}
return data.output;
}
/**
* @private
* @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertChildren
*/
_convertChildren( input, consumable, additionalData = {} ) {
// Get all children of view input item.
const viewChildren = Array.from( input.getChildren() );
// 1. Map those children to model.
// 2. Filter out items that has not been converted or for which conversion returned wrong result (for those warning is logged).
// 3. Extract children from document fragments to flatten results.
const convertedChildren = viewChildren
.map( viewChild => this._convertItem( viewChild, consumable, additionalData ) )
.filter( converted => converted instanceof node_Node || converted instanceof documentfragment_DocumentFragment )
.reduce( ( result, filtered ) => {
return result.concat(
filtered.is( 'documentFragment' ) ? Array.from( filtered.getChildren() ) : filtered
);
}, [] );
// Normalize array to model document fragment.
return new documentfragment_DocumentFragment( convertedChildren );
}
/**
* Fired before the first conversion event, at the beginning of view to model conversion process.
*
* @event viewCleanup
* @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element}
* viewItem Part of the view to be converted.
*/
/**
* Fired when {@link module:engine/view/element~Element} is converted.
*
* `element` is a namespace event for a class of events. Names of actually called events follow this pattern:
* `element:<elementName>` where `elementName` is the name of converted element. This way listeners may listen to
* all elements conversion or to conversion of specific elements.
*
* @event element
* @param {Object} data Object containing conversion input and a placeholder for conversion output and possibly other
* values (see {@link #convert}).
* Keep in mind that this object is shared by reference between all callbacks that will be called.
* This means that callbacks can add their own values if needed,
* and those values will be available in other callbacks.
* @param {module:engine/view/element~Element} data.input Converted element.
* @param {*} data.output The current state of conversion result. Every change to converted element should
* be reflected by setting or modifying this property.
* @param {module:engine/model/schema~SchemaPath} data.context The conversion context.
* @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ViewConversionDispatcher` constructor.
* Besides of properties passed in constructor, it also has `convertItem` and `convertChildren` methods which are references
* to {@link #_convertItem} and
* {@link ~ViewConversionDispatcher#_convertChildren}. Those methods are needed to convert
* the whole view-tree they were exposed in `conversionApi` for callbacks.
*/
/**
* Fired when {@link module:engine/view/text~Text} is converted.
*
* @event text
* @see #event:element
*/
/**
* Fired when {@link module:engine/view/documentfragment~DocumentFragment} is converted.
*
* @event documentFragment
* @see #event:element
*/
}
mix( viewconversiondispatcher_ViewConversionDispatcher, emittermixin );
// Traverses given model item and searches elements which marks marker range. Found element is removed from
// DocumentFragment but path of this element is stored in a Map which is then returned.
//
// @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/node~Node} modelItem Fragment of model.
// @returns {Map<String, module:engine/model/range~Range>} List of static markers.
function extractMarkersFromModelFragment( modelItem ) {
const markerElements = new Set();
const markers = new Map();
// Create ModelTreeWalker.
const walker = new treewalker_TreeWalker( {
startPosition: position_Position.createAt( modelItem, 0 ),
ignoreElementEnd: true
} );
// Walk through DocumentFragment and collect marker elements.
for ( const value of walker ) {
// Check if current element is a marker.
if ( value.item.name == '$marker' ) {
markerElements.add( value.item );
}
}
// Walk through collected marker elements store its path and remove its from the DocumentFragment.
for ( const markerElement of markerElements ) {
const markerName = markerElement.getAttribute( 'data-name' );
const currentPosition = position_Position.createBefore( markerElement );
// When marker of given name is not stored it means that we have found the beginning of the range.
if ( !markers.has( markerName ) ) {
markers.set( markerName, new range_Range( position_Position.createFromPosition( currentPosition ) ) );
// Otherwise is means that we have found end of the marker range.
} else {
markers.get( markerName ).end = position_Position.createFromPosition( currentPosition );
}
// Remove marker element from DocumentFragment.
model_writer_remove( range_Range.createOn( markerElement ) );
}
return markers;
}
/**
* Conversion interface that is registered for given {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher}
* and is passed as one of parameters when {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher dispatcher}
* fires it's events.
*
* `ViewConversionApi` object is built by {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher} constructor.
* The exact list of properties of this object is determined by the object passed to the constructor.
*
* @interface ViewConversionApi
*/
/**
* Starts conversion of given item by firing an appropriate event.
*
* Every fired event is passed (as first parameter) an object with `output` property. Every event may set and/or
* modify that property. When all callbacks are done, the final value of `output` property is returned by this method.
* The `output` must be either {@link module:engine/model/node~Node model node} or
* {@link module:engine/model/documentfragment~DocumentFragment model document fragment} or `null` (as set by default).
*
* @method #convertItem
* @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element
* @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:text
* @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:documentFragment
* @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element|module:engine/view/text~Text}
* input Item to convert.
* @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
* @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
* events. See also {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element element event}.
* @returns {module:engine/model/node~Node|module:engine/model/documentfragment~DocumentFragment|null} The result of item conversion,
* created and modified by callbacks attached to fired event, or `null` if the conversion result was incorrect.
*/
/**
* Starts conversion of all children of given item by firing appropriate events for all those children.
*
* @method #convertChildren
* @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element
* @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:text
* @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:documentFragment
* @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element}
* input Item which children will be converted.
* @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
* @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
* events. See also {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element element event}.
* @returns {module:engine/model/documentfragment~DocumentFragment} Model document fragment containing results of conversion
* of all children of given item.
*/
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/conversion/view-to-model-converters.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
* {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher}.
*
* @module engine/conversion/view-to-model-converters
*/
/**
* Function factory, creates a converter that converts {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
* or all children of {@link module:engine/view/element~Element} into
* {@link module:engine/model/documentfragment~DocumentFragment model document fragment}.
* This is the "entry-point" converter for view to model conversion. This converter starts the conversion of all children
* of passed view document fragment. Those children {@link module:engine/view/node~Node view nodes} are then handled by other converters.
*
* This also a "default", last resort converter for all view elements that has not been converted by other converters.
* When a view element is being converted to the model but it does not have converter specified, that view element
* will be converted to {@link module:engine/model/documentfragment~DocumentFragment model document fragment} and returned.
*
* @returns {Function} Universal converter for view {@link module:engine/view/documentfragment~DocumentFragment fragments} and
* {@link module:engine/view/element~Element elements} that returns
* {@link module:engine/model/documentfragment~DocumentFragment model fragment} with children of converted view item.
*/
function convertToModelFragment() {
return ( evt, data, consumable, conversionApi ) => {
// Second argument in `consumable.consume` is discarded for ViewDocumentFragment but is needed for ViewElement.
if ( !data.output && consumable.consume( data.input, { name: true } ) ) {
const convertedChildren = conversionApi.convertChildren( data.input, consumable, data );
data.output = new documentfragment_DocumentFragment( normalizeNodes( convertedChildren ) );
}
};
}
/**
* Function factory, creates a converter that converts {@link module:engine/view/text~Text} to {@link module:engine/model/text~Text}.
*
* @returns {Function} {@link module:engine/view/text~Text View text} converter.
*/
function convertText() {
return ( evt, data, consumable, conversionApi ) => {
const schemaQuery = {
name: '$text',
inside: data.context
};
if ( conversionApi.schema.check( schemaQuery ) ) {
if ( consumable.consume( data.input ) ) {
data.output = new text_Text( data.input.data );
}
}
};
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/liveposition.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/liveposition
*/
/**
* `LivePosition` is a type of {@link module:engine/model/position~Position Position}
* that updates itself as {@link module:engine/model/document~Document document}
* is changed through operations. It may be used as a bookmark.
*
* **Note:** Contrary to {@link module:engine/model/position~Position}, `LivePosition` works only in roots that are
* {@link module:engine/model/rootelement~RootElement}.
* If {@link module:engine/model/documentfragment~DocumentFragment} is passed, error will be thrown.
*
* **Note:** Be very careful when dealing with `LivePosition`. Each `LivePosition` instance bind events that might
* have to be unbound.
* Use {@link module:engine/model/liveposition~LivePosition#detach} whenever you don't need `LivePosition` anymore.
*
* @extends module:engine/model/position~Position
*/
class liveposition_LivePosition extends position_Position {
/**
* Creates a live position.
*
* @see module:engine/model/position~Position
* @param {module:engine/model/rootelement~RootElement} root
* @param {Array.<Number>} path
* @param {module:engine/model/position~PositionStickiness} [stickiness] Defaults to `'sticksToNext'`.
* See {@link module:engine/model/liveposition~LivePosition#stickiness}.
*/
constructor( root, path, stickiness ) {
super( root, path );
if ( !this.root.is( 'rootElement' ) ) {
/**
* LivePosition's root has to be an instance of RootElement.
*
* @error liveposition-root-not-rootelement
*/
throw new CKEditorError(
'model-liveposition-root-not-rootelement: LivePosition\'s root has to be an instance of RootElement.'
);
}
/**
* Flag representing `LivePosition` stickiness. `LivePosition` might be sticking to previous node or next node.
* Whenever some nodes are inserted at the same position as `LivePosition`, `stickiness` is checked to decide if
* LivePosition should be moved. Similar applies when a range of nodes is moved and one of it's boundary
* position is same as `LivePosition`.
*
* Examples:
*
* Insert:
* Position is at | and we insert at the same position, marked as ^:
* - | sticks to previous node: `<p>f|^oo</p>` => `<p>f|baroo</p>`
* - | sticks to next node: `<p>f^|oo</p>` => `<p>fbar|oo</p>`
*
* Move:
* Position is at | and range [ ] is moved to position ^:
* - | sticks to previous node: `<p>f|[oo]</p><p>b^ar</p>` => `<p>f|</p><p>booar</p>`
* - | sticks to next node: `<p>f|[oo]</p><p>b^ar</p>` => `<p>f</p><p>b|ooar</p>`
*
* @member {module:engine/model/position~PositionStickiness} module:engine/model/liveposition~LivePosition#stickiness
*/
this.stickiness = stickiness || 'sticksToNext';
bindWithDocument.call( this );
}
/**
* Unbinds all events previously bound by `LivePosition`. Use it whenever you don't need `LivePosition` instance
* anymore (i.e. when leaving scope in which it was declared or before re-assigning variable that was
* referring to it).
*/
detach() {
this.stopListening();
}
/**
* @static
* @method module:engine/model/liveposition~LivePosition.createAfter
* @see module:engine/model/position~Position.createAfter
* @param {module:engine/model/node~Node} node
* @returns {module:engine/model/liveposition~LivePosition}
*/
/**
* @static
* @method module:engine/model/liveposition~LivePosition.createBefore
* @see module:engine/model/position~Position.createBefore
* @param {module:engine/model/node~Node} node
* @returns {module:engine/model/liveposition~LivePosition}
*/
/**
* @static
* @method module:engine/model/liveposition~LivePosition.createFromParentAndOffset
* @see module:engine/model/position~Position.createFromParentAndOffset
* @param {module:engine/model/element~Element} parent
* @param {Number} offset
* @returns {module:engine/model/liveposition~LivePosition}
*/
/**
* @static
* @method module:engine/model/liveposition~LivePosition.createFromPosition
* @see module:engine/model/position~Position.createFromPosition
* @param {module:engine/model/position~Position} position
* @returns {module:engine/model/liveposition~LivePosition}
*/
/**
* Fired when `LivePosition` instance is changed due to changes on {@link module:engine/model/document~Document}.
*
* @event module:engine/model/liveposition~LivePosition#change
* @param {module:engine/model/position~Position} oldPosition Position equal to this live position before it got changed.
*/
}
/**
* Binds this `LivePosition` to the {@link module:engine/model/document~Document document} that owns
* this position's {@link module:engine/model/position~Position#root root}.
*
* @ignore
* @private
* @method module:engine/model/liveposition~LivePosition.bindWithDocument
*/
function bindWithDocument() {
// Operation types handled by LivePosition (these are operations that change model tree structure).
const supportedTypes = new Set( [ 'insert', 'move', 'remove', 'reinsert' ] );
this.listenTo(
this.root.document,
'change',
( event, type, changes ) => {
if ( supportedTypes.has( type ) ) {
transform.call( this, type, changes.range, changes.sourcePosition );
}
},
{ priority: 'high' }
);
}
/**
* Updates this position accordingly to the updates applied to the model. Bases on change events.
*
* @ignore
* @private
* @method transform
* @param {String} type Type of changes applied to the Tree Model.
* @param {module:engine/model/range~Range} range Range containing the result of applied change.
* @param {module:engine/model/position~Position} [position] Additional position parameter provided by some change events.
*/
function transform( type, range, position ) {
/* eslint-disable no-case-declarations */
const howMany = range.end.offset - range.start.offset;
let transformed;
switch ( type ) {
case 'insert':
const insertBefore = this.stickiness == 'sticksToNext';
transformed = this._getTransformedByInsertion( range.start, howMany, insertBefore );
break;
case 'move':
case 'remove':
case 'reinsert':
const originalRange = range_Range.createFromPositionAndShift( position, howMany );
const gotMoved = originalRange.containsPosition( this ) ||
( originalRange.start.isEqual( this ) && this.stickiness == 'sticksToNext' ) ||
( originalRange.end.isEqual( this ) && this.stickiness == 'sticksToPrevious' );
// We can't use ._getTransformedByMove() because we have a different if-condition.
if ( gotMoved ) {
transformed = this._getCombined( position, range.start );
} else {
const insertBefore = this.stickiness == 'sticksToNext';
transformed = this._getTransformedByMove( position, range.start, howMany, insertBefore );
}
break;
}
if ( !this.isEqual( transformed ) ) {
const oldPosition = position_Position.createFromPosition( this );
this.path = transformed.path;
this.root = transformed.root;
this.fire( 'change', oldPosition );
}
/* eslint-enable no-case-declarations */
}
mix( liveposition_LivePosition, emittermixin );
/**
* Enum representing how position is "sticking" with their neighbour nodes.
* Possible values: `'sticksToNext'`, `'sticksToPrevious'`.
*
* @typedef {String} module:engine/model/position~PositionStickiness
*/
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/controller/insertcontent.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/controller/insertcontent
*/
/**
* Inserts content into the editor (specified selection) as one would expect the paste
* functionality to work.
*
* **Note:** Use {@link module:engine/controller/datacontroller~DataController#insertContent} instead of this function.
* This function is only exposed to be reusable in algorithms
* which change the {@link module:engine/controller/datacontroller~DataController#insertContent}
* method's behavior.
*
* @param {module:engine/controller/datacontroller~DataController} dataController The data controller in context of which the insertion
* should be performed.
* @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
* @param {module:engine/model/selection~Selection} selection Selection into which the content should be inserted.
* @param {module:engine/model/batch~Batch} [batch] Batch to which deltas will be added. If not specified, then
* changes will be added to a new batch.
*/
function insertContent( dataController, content, selection, batch ) {
if ( !batch ) {
batch = dataController.model.batch();
}
if ( !selection.isCollapsed ) {
dataController.deleteContent( selection, batch );
}
const insertion = new insertcontent_Insertion( dataController, batch, selection.anchor );
let nodesToInsert;
if ( content.is( 'documentFragment' ) ) {
nodesToInsert = content.getChildren();
} else {
nodesToInsert = [ content ];
}
insertion.handleNodes( nodesToInsert, {
// The set of children being inserted is the only set in this context
// so it's the first and last (it's a hack ;)).
isFirst: true,
isLast: true
} );
const newRange = insertion.getSelectionRange();
/* istanbul ignore else */
if ( newRange ) {
selection.setRanges( [ newRange ] );
} else {
// We are not testing else because it's a safe check for unpredictable edge cases:
// an insertion without proper range to select.
/**
* Cannot determine a proper selection range after insertion.
*
* @warning insertcontent-no-range
*/
src_log.warn( 'insertcontent-no-range: Cannot determine a proper selection range after insertion.' );
}
}
/**
* Utility class for performing content insertion.
*
* @private
*/
class insertcontent_Insertion {
constructor( dataController, batch, position ) {
/**
* The data controller in context of which the insertion should be performed.
*
* @member {module:engine/controller/datacontroller~DataController} #dataController
*/
this.dataController = dataController;
/**
* Batch to which deltas will be added.
*
* @member {module:engine/controller/batch~Batch} #batch
*/
this.batch = batch;
/**
* The position at which (or near which) the next node will be inserted.
*
* @member {module:engine/model/position~Position} #position
*/
this.position = position;
/**
* Elements with which the inserted elements can be merged.
*
* <p>x^</p><p>y</p> + <p>z</p> (can merge to <p>x</p>)
* <p>x</p><p>^y</p> + <p>z</p> (can merge to <p>y</p>)
* <p>x^y</p> + <p>z</p> (can merge to <p>xy</p> which will be split during the action,
* so both its pieces will be added to this set)
*
*
* @member {Set} #canMergeWith
*/
this.canMergeWith = new Set( [ this.position.parent ] );
/**
* Schema of the model.
*
* @member {module:engine/model/schema~Schema} #schema
*/
this.schema = dataController.model.schema;
}
/**
* Handles insertion of a set of nodes.
*
* @param {Iterable.<module:engine/model/node~Node>} nodes Nodes to insert.
* @param {Object} parentContext Context in which parent of these nodes was supposed to be inserted.
* If the parent context is passed it means that the parent element was stripped (was not allowed).
*/
handleNodes( nodes, parentContext ) {
nodes = Array.from( nodes );
for ( let i = 0; i < nodes.length; i++ ) {
const node = nodes[ i ];
this._handleNode( node, {
isFirst: i === 0 && parentContext.isFirst,
isLast: ( i === ( nodes.length - 1 ) ) && parentContext.isLast
} );
}
}
/**
* Returns range to be selected after insertion.
* Returns null if there is no valid range to select after insertion.
*
* @returns {module:engine/model/range~Range|null}
*/
getSelectionRange() {
if ( this.nodeToSelect ) {
return range_Range.createOn( this.nodeToSelect );
}
return this.dataController.model.getNearestSelectionRange( this.position );
}
/**
* Handles insertion of a single node.
*
* @param {module:engine/model/node~Node} node
* @param {Object} context
* @param {Boolean} context.isFirst Whether the given node is the first one in the content to be inserted.
* @param {Boolean} context.isLast Whether the given node is the last one in the content to be inserted.
*/
_handleNode( node, context ) {
// Let's handle object in a special way.
// * They should never be merged with other elements.
// * If they are not allowed in any of the selection ancestors, they could be either autoparagraphed or totally removed.
if ( this._checkIsObject( node ) ) {
this._handleObject( node, context );
return;
}
// Try to find a place for the given node.
// Split the position.parent's branch up to a point where the node can be inserted.
// If it isn't allowed in the whole branch, then of course don't split anything.
const isAllowed = this._checkAndSplitToAllowedPosition( node, context );
if ( !isAllowed ) {
this._handleDisallowedNode( node, context );
return;
}
this._insert( node );
// After the node was inserted we may try to merge it with its siblings.
// This should happen only if it was the first and/or last of the nodes (so only with boundary nodes)
// and only if the selection was in those elements initially.
//
// E.g.:
// <p>x^</p> + <p>y</p> => <p>x</p><p>y</p> => <p>xy[]</p>
// and:
// <p>x^y</p> + <p>z</p> => <p>x</p>^<p>y</p> + <p>z</p> => <p>x</p><p>y</p><p>z</p> => <p>xy[]z</p>
// but:
// <p>x</p><p>^</p><p>z</p> + <p>y</p> => <p>x</p><p>y</p><p>z</p> (no merging)
// <p>x</p>[<img>]<p>z</p> + <p>y</p> => <p>x</p><p>y</p><p>z</p> (no merging, note: after running deletetContents
// it's exactly the same case as above)
this._mergeSiblingsOf( node, context );
}
/**
* @param {module:engine/model/element~Element} node The object element.
* @param {Object} context
*/
_handleObject( node, context ) {
// Try finding it a place in the tree.
if ( this._checkAndSplitToAllowedPosition( node ) ) {
this._insert( node );
}
// Try autoparagraphing.
else {
this._tryAutoparagraphing( node, context );
}
}
/**
* @param {module:engine/model/node~Node} node The disallowed node which needs to be handled.
* @param {Object} context
*/
_handleDisallowedNode( node, context ) {
// If the node is an element, try inserting its children (strip the parent).
if ( node.is( 'element' ) ) {
this.handleNodes( node.getChildren(), context );
}
// If the node is a text and bare text is allowed in current position it means that the node
// contains disallowed attributes and we have to remove them.
else if ( this.schema.check( { name: '$text', inside: this.position } ) ) {
this.schema.removeDisallowedAttributes( [ node ], this.position );
this._handleNode( node, context );
}
// If text is not allowed, try autoparagraphing.
else {
this._tryAutoparagraphing( node, context );
}
}
/**
* @param {module:engine/model/node~Node} node The node to insert.
*/
_insert( node ) {
/* istanbul ignore if */
if ( !this._checkIsAllowed( node, this.position ) ) {
// Algorithm's correctness check. We should never end up here but it's good to know that we did.
// Note that it would often be a silent issue if we insert node in a place where it's not allowed.
src_log.error(
'insertcontent-wrong-position: The node cannot be inserted on the given position.',
{ node, position: this.position }
);
return;
}
const livePos = liveposition_LivePosition.createFromPosition( this.position );
this.batch.insert( this.position, node );
this.position = position_Position.createFromPosition( livePos );
livePos.detach();
// The last inserted object should be selected because we can't put a collapsed selection after it.
if ( this._checkIsObject( node ) && !this.schema.check( { name: '$text', inside: this.position } ) ) {
this.nodeToSelect = node;
} else {
this.nodeToSelect = null;
}
}
/**
* @param {module:engine/model/node~Node} node The node which could potentially be merged.
* @param {Object} context
*/
_mergeSiblingsOf( node, context ) {
if ( !( node instanceof element_Element ) ) {
return;
}
const mergeLeft = context.isFirst && ( node.previousSibling instanceof element_Element ) && this.canMergeWith.has( node.previousSibling );
const mergeRight = context.isLast && ( node.nextSibling instanceof element_Element ) && this.canMergeWith.has( node.nextSibling );
const mergePosLeft = liveposition_LivePosition.createBefore( node );
const mergePosRight = liveposition_LivePosition.createAfter( node );
if ( mergeLeft ) {
const position = liveposition_LivePosition.createFromPosition( this.position );
this.batch.merge( mergePosLeft );
// We need to check and strip disallowed attributes in all nested nodes because after merge
// some attributes could end up in a path where are disallowed.
const parent = position.nodeBefore;
this.schema.removeDisallowedAttributes( parent.getChildren(), position_Position.createAt( parent ), this.batch );
this.position = position_Position.createFromPosition( position );
position.detach();
}
if ( mergeRight ) {
/* istanbul ignore if */
if ( !this.position.isEqual( mergePosRight ) ) {
// Algorithm's correctness check. We should never end up here but it's good to know that we did.
// At this point the insertion position should be after the node we'll merge. If it isn't,
// it should need to be secured as in the left merge case.
src_log.error( 'insertcontent-wrong-position-on-merge: The insertion position should equal the merge position' );
}
// Move the position to the previous node, so it isn't moved to the graveyard on merge.
// <p>x</p>[]<p>y</p> => <p>x[]</p><p>y</p>
this.position = position_Position.createAt( mergePosRight.nodeBefore, 'end' );
// OK: <p>xx[]</p> + <p>yy</p> => <p>xx[]yy</p> (when sticks to previous)
// NOK: <p>xx[]</p> + <p>yy</p> => <p>xxyy[]</p> (when sticks to next)
const position = new liveposition_LivePosition( this.position.root, this.position.path, 'sticksToPrevious' );
this.batch.merge( mergePosRight );
// We need to check and strip disallowed attributes in all nested nodes because after merge
// some attributes could end up in a place where are disallowed.
this.schema.removeDisallowedAttributes( position.parent.getChildren(), position, this.batch );
this.position = position_Position.createFromPosition( position );
position.detach();
}
mergePosLeft.detach();
mergePosRight.detach();
// When there was no merge we need to check and strip disallowed attributes in all nested nodes of
// just inserted node because some attributes could end up in a place where are disallowed.
if ( !mergeLeft && !mergeRight ) {
this.schema.removeDisallowedAttributes( node.getChildren(), position_Position.createAt( node ), this.batch );
}
}
/**
* Tries wrapping the node in a new paragraph and inserting it this way.
*
* @param {module:engine/model/node~Node} node The node which needs to be autoparagraphed.
* @param {Object} context
*/
_tryAutoparagraphing( node, context ) {
const paragraph = new element_Element( 'paragraph' );
// Do not autoparagraph if the paragraph won't be allowed there,
// cause that would lead to an infinite loop. The paragraph would be rejected in
// the next _handleNode() call and we'd be here again.
if ( this._getAllowedIn( paragraph, this.position.parent ) ) {
// When node is a text and is disallowed by schema it means that contains disallowed attributes
// and we need to remove them.
if ( node.is( 'text' ) && !this._checkIsAllowed( node, [ paragraph ] ) ) {
this.schema.removeDisallowedAttributes( [ node ], [ paragraph ] );
}
if ( this._checkIsAllowed( node, [ paragraph ] ) ) {
paragraph.appendChildren( node );
this._handleNode( paragraph, context );
}
}
}
/**
* @param {module:engine/model/node~Node} node
* @returns {Boolean} Whether an allowed position was found.
* `false` is returned if the node isn't allowed at any position up in the tree, `true` if was.
*/
_checkAndSplitToAllowedPosition( node ) {
const allowedIn = this._getAllowedIn( node, this.position.parent );
if ( !allowedIn ) {
return false;
}
while ( allowedIn != this.position.parent ) {
// If a parent which we'd need to leave is a limit element, break.
if ( this.schema.limits.has( this.position.parent.name ) ) {
return false;
}
if ( this.position.isAtStart ) {
const parent = this.position.parent;
this.position = position_Position.createBefore( parent );
// Special case – parent is empty (<p>^</p>) so isAtStart == isAtEnd == true.
// We can remove the element after moving selection out of it.
if ( parent.isEmpty ) {
this.batch.remove( parent );
}
} else if ( this.position.isAtEnd ) {
this.position = position_Position.createAfter( this.position.parent );
} else {
const tempPos = position_Position.createAfter( this.position.parent );
this.batch.split( this.position );
this.position = tempPos;
this.canMergeWith.add( this.position.nodeAfter );
}
}
return true;
}
/**
* Gets the element in which the given node is allowed. It checks the passed element and all its ancestors.
*
* @param {module:engine/model/node~Node} node The node to check.
* @param {module:engine/model/element~Element} element The element in which the node's correctness should be checked.
* @returns {module:engine/model/element~Element|null}
*/
_getAllowedIn( node, element ) {
if ( this._checkIsAllowed( node, [ element ] ) ) {
return element;
}
if ( element.parent ) {
return this._getAllowedIn( node, element.parent );
}
return null;
}
/**
* Check whether the given node is allowed in the specified schema path.
*
* @param {module:engine/model/node~Node} node
* @param {module:engine/model/schema~SchemaPath} path
*/
_checkIsAllowed( node, path ) {
return this.schema.check( {
name: getNodeSchemaName( node ),
attributes: Array.from( node.getAttributeKeys() ),
inside: path
} );
}
/**
* Checks whether according to the schema this is an object type element.
*
* @param {module:engine/model/node~Node} node The node to check.
*/
_checkIsObject( node ) {
return this.schema.objects.has( getNodeSchemaName( node ) );
}
}
// Gets a name under which we should check this node in the schema.
//
// @param {module:engine/model/node~Node} node The node.
// @returns {String} Node name.
function getNodeSchemaName( node ) {
return node.is( 'text' ) ? '$text' : node.name;
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/controller/deletecontent.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/controller/deletecontent
*/
/**
* Deletes content of the selection and merge siblings. The resulting selection is always collapsed.
*
* @param {module:engine/model/selection~Selection} selection Selection of which the content should be deleted.
* @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added.
* @param {Object} [options]
* @param {Boolean} [options.leaveUnmerged=false] Whether to merge elements after removing the content of the selection.
*
* For example `<heading>x[x</heading><paragraph>y]y</paragraph>` will become:
*
* * `<heading>x^y</heading>` with the option disabled (`leaveUnmerged == false`)
* * `<heading>x^</heading><paragraph>y</paragraph>` with enabled (`leaveUnmerged == true`).
*
* Note: {@link module:engine/model/schema~Schema#objects object} and {@link module:engine/model/schema~Schema#limits limit}
* elements will not be merged.
*
* @param {Boolean} [options.doNotResetEntireContent=false] Whether to skip replacing the entire content with a
* paragraph when the entire content was selected.
*
* For example `<heading>[x</heading><paragraph>y]</paragraph> will become:
*
* * `<paragraph>^</paragraph>` with the option disabled (`doNotResetEntireContent == false`)
* * `<heading>^</heading>` with enabled (`doNotResetEntireContent == true`).
*/
function deleteContent( selection, batch, options = {} ) {
if ( selection.isCollapsed ) {
return;
}
const schema = batch.document.schema;
// 1. Replace the entire content with paragraph.
// See: https://github.com/ckeditor/ckeditor5-engine/issues/1012#issuecomment-315017594.
if ( !options.doNotResetEntireContent && shouldEntireContentBeReplacedWithParagraph( schema, selection ) ) {
replaceEntireContentWithParagraph( batch, selection );
return;
}
const selRange = selection.getFirstRange();
const startPos = selRange.start;
const endPos = liveposition_LivePosition.createFromPosition( selRange.end );
// 2. Remove the content if there is any.
if ( !selRange.start.isTouching( selRange.end ) ) {
batch.remove( selRange );
}
// 3. Merge elements in the right branch to the elements in the left branch.
// The only reasonable (in terms of data and selection correctness) case in which we need to do that is:
//
// <heading type=1>Fo[</heading><paragraph>]ar</paragraph> => <heading type=1>Fo^ar</heading>
//
// However, the algorithm supports also merging deeper structures (up to the depth of the shallower branch),
// as it's hard to imagine what should actually be the default behavior. Usually, specific features will
// want to override that behavior anyway.
if ( !options.leaveUnmerged ) {
mergeBranches( batch, startPos, endPos );
// We need to check and strip disallowed attributes in all nested nodes because after merge
// some attributes could end up in a path where are disallowed.
//
// e.g. bold is disallowed for <H1>
// <h1>Fo{o</h1><p>b}a<b>r</b><p> -> <h1>Fo{}a<b>r</b><h1> -> <h1>Fo{}ar<h1>.
schema.removeDisallowedAttributes( startPos.parent.getChildren(), startPos, batch );
}
selection.setCollapsedAt( startPos );
// 4. Autoparagraphing.
// Check if a text is allowed in the new container. If not, try to create a new paragraph (if it's allowed here).
if ( deletecontent_shouldAutoparagraph( schema, startPos ) ) {
insertParagraph( batch, startPos, selection );
}
endPos.detach();
}
// This function is a result of reaching the Ballmer's peak for just the right amount of time.
// Even I had troubles documenting it after a while and after reading it again I couldn't believe that it really works.
function mergeBranches( batch, startPos, endPos ) {
const startParent = startPos.parent;
const endParent = endPos.parent;
// If both positions ended up in the same parent, then there's nothing more to merge:
// <$root><p>x[]</p><p>{}y</p></$root> => <$root><p>xy</p>[]{}</$root>
if ( startParent == endParent ) {
return;
}
// If one of the positions is a root, then there's nothing more to merge (at least in the current state of implementation).
// Theoretically in this case we could unwrap the <p>: <$root>x[]<p>{}y</p></$root>, but we don't need to support it yet
// so let's just abort.
if ( !startParent.parent || !endParent.parent ) {
return;
}
// Check if operations we'll need to do won't need to cross object or limit boundaries.
// E.g., we can't merge endParent into startParent in this case:
// <limit><startParent>x[]</startParent></limit><endParent>{}</endParent>
if ( !checkCanBeMerged( startPos, endPos ) ) {
return;
}
// Remember next positions to merge. For example:
// <a><b>x[]</b></a><c><d>{}y</d></c>
// will become:
// <a><b>xy</b>[]</a><c>{}</c>
startPos = position_Position.createAfter( startParent );
endPos = position_Position.createBefore( endParent );
if ( !endPos.isEqual( startPos ) ) {
// In this case, before we merge, we need to move `endParent` to the `startPos`:
// <a><b>x[]</b></a><c><d>{}y</d></c>
// becomes:
// <a><b>x</b>[]<d>y</d></a><c>{}</c>
batch.move( endParent, startPos );
}
// Merge two siblings:
// <a>x</a>[]<b>y</b> -> <a>xy</a> (the usual case)
// <a><b>x</b>[]<d>y</d></a><c></c> -> <a><b>xy</b>[]</a><c></c> (this is the "move parent" case shown above)
batch.merge( startPos );
// Remove empty end ancestors:
// <a>fo[o</a><b><a><c>bar]</c></a></b>
// becomes:
// <a>fo[]</a><b><a>{}</a></b>
// So we can remove <a> and <b>.
while ( endPos.parent.isEmpty ) {
const parentToRemove = endPos.parent;
endPos = position_Position.createBefore( parentToRemove );
batch.remove( parentToRemove );
}
// Continue merging next level.
mergeBranches( batch, startPos, endPos );
}
function deletecontent_shouldAutoparagraph( schema, position ) {
const isTextAllowed = schema.check( { name: '$text', inside: position } );
const isParagraphAllowed = schema.check( { name: 'paragraph', inside: position } );
return !isTextAllowed && isParagraphAllowed;
}
// Check if parents of two positions can be merged by checking if there are no limit/object
// boundaries between those two positions.
//
// E.g. in <bQ><p>x[]</p></bQ><widget><caption>{}</caption></widget>
// we'll check <p>, <bQ>, <widget> and <caption>.
// Usually, widget and caption are marked as objects/limits in the schema, so in this case merging will be blocked.
function checkCanBeMerged( leftPos, rightPos ) {
const schema = leftPos.root.document.schema;
const rangeToCheck = new range_Range( leftPos, rightPos );
for ( const value of rangeToCheck.getWalker() ) {
if ( schema.objects.has( value.item.name ) || schema.limits.has( value.item.name ) ) {
return false;
}
}
return true;
}
function insertParagraph( batch, position, selection ) {
const paragraph = new element_Element( 'paragraph' );
batch.insert( position, paragraph );
selection.setCollapsedAt( paragraph );
}
function replaceEntireContentWithParagraph( batch, selection ) {
const limitElement = batch.document.schema.getLimitElement( selection );
batch.remove( range_Range.createIn( limitElement ) );
insertParagraph( batch, position_Position.createAt( limitElement ), selection );
}
// We want to replace the entire content with a paragraph when:
// * the entire content is selected,
// * selection contains at least two elements,
// * whether the paragraph is allowed in schema in the common ancestor.
function shouldEntireContentBeReplacedWithParagraph( schema, selection ) {
const limitElement = schema.getLimitElement( selection );
if ( !selection.containsEntireContent( limitElement ) ) {
return false;
}
const range = selection.getFirstRange();
if ( range.start.parent == range.end.parent ) {
return false;
}
return schema.check( { name: 'paragraph', inside: limitElement.name } );
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/unicode.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* Set of utils to handle unicode characters.
*
* @module utils/unicode
*/
/**
* Checks whether given `character` is a combining mark.
*
* @param {String} character Character to check.
* @returns {Boolean}
*/
function isCombiningMark( character ) {
return !!character && character.length == 1 && /[\u0300-\u036f\u1ab0-\u1aff\u1dc0-\u1dff\u20d0-\u20ff\ufe20-\ufe2f]/.test( character );
}
/**
* Checks whether given `character` is a high half of surrogate pair.
*
* Using UTF-16 terminology, a surrogate pair denotes UTF-16 character using two UTF-8 characters. The surrogate pair
* consist of high surrogate pair character followed by low surrogate pair character.
*
* @param {String} character Character to check.
* @returns {Boolean}
*/
function isHighSurrogateHalf( character ) {
return !!character && character.length == 1 && /[\ud800-\udbff]/.test( character );
}
/**
* Checks whether given `character` is a low half of surrogate pair.
*
* Using UTF-16 terminology, a surrogate pair denotes UTF-16 character using two UTF-8 characters. The surrogate pair
* consist of high surrogate pair character followed by low surrogate pair character.
*
* @param {String} character Character to check.
* @returns {Boolean}
*/
function isLowSurrogateHalf( character ) {
return !!character && character.length == 1 && /[\udc00-\udfff]/.test( character );
}
/**
* Checks whether given offset in a string is inside a surrogate pair (between two surrogate halves).
*
* @param {String} string String to check.
* @param {Number} offset Offset to check.
* @returns {Boolean}
*/
function isInsideSurrogatePair( string, offset ) {
return isHighSurrogateHalf( string.charAt( offset - 1 ) ) && isLowSurrogateHalf( string.charAt( offset ) );
}
/**
* Checks whether given offset in a string is between base character and combining mark or between two combining marks.
*
* @param {String} string String to check.
* @param {Number} offset Offset to check.
* @returns {Boolean}
*/
function isInsideCombinedSymbol( string, offset ) {
return isCombiningMark( string.charAt( offset ) );
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/controller/modifyselection.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/controller/modifyselection
*/
/**
* Modifies the selection. Currently, the supported modifications are:
*
* * Extending. The selection focus is moved in the specified `options.direction` with a step specified in `options.unit`.
* Possible values for `unit` are:
* * `'character'` (default) - moves selection by one user-perceived character. In most cases this means moving by one
* character in `String` sense. However, unicode also defines "combing marks". These are special symbols, that combines
* with a symbol before it ("base character") to create one user-perceived character. For example, `q̣̇` is a normal
* letter `q` with two "combining marks": upper dot (`Ux0307`) and lower dot (`Ux0323`). For most actions, i.e. extending
* selection by one position, it is correct to include both "base character" and all of it's "combining marks". That is
* why `'character'` value is most natural and common method of modifying selection.
* * `'codePoint'` - moves selection by one unicode code point. In contrary to, `'character'` unit, this will insert
* selection between "base character" and "combining mark", because "combining marks" have their own unicode code points.
* However, for technical reasons, unicode code points with values above `UxFFFF` are represented in native `String` by
* two characters, called "surrogate pairs". Halves of "surrogate pairs" have a meaning only when placed next to each other.
* For example `𨭎` is represented in `String` by `\uD862\uDF4E`. Both `\uD862` and `\uDF4E` do not have any meaning
* outside the pair (are rendered as ? when alone). Position between them would be incorrect. In this case, selection
* extension will include whole "surrogate pair".
*
* **Note:** if you extend a forward selection in a backward direction you will in fact shrink it.
*
* @param {module:engine/controller/datacontroller~DataController} dataController The data controller in context of which
* the selection modification should be performed.
* @param {module:engine/model/selection~Selection} selection The selection to modify.
* @param {Object} [options]
* @param {'forward'|'backward'} [options.direction='forward'] The direction in which the selection should be modified.
* @param {'character'|'codePoint'} [options.unit='character'] The unit by which selection should be modified.
*/
function modifySelection( dataController, selection, options = {} ) {
const schema = dataController.model.schema;
const isForward = options.direction != 'backward';
const unit = options.unit ? options.unit : 'character';
const focus = selection.focus;
const walker = new treewalker_TreeWalker( {
boundaries: getSearchRange( focus, isForward ),
singleCharacters: true,
direction: isForward ? 'forward' : 'backward'
} );
const data = { walker, schema, isForward, unit };
let next;
while ( ( next = walker.next() ) ) {
if ( next.done ) {
return;
}
const position = tryExtendingTo( data, next.value );
if ( position ) {
selection.moveFocusTo( position );
return;
}
}
}
// Checks whether the selection can be extended to the the walker's next value (next position).
function tryExtendingTo( data, value ) {
// If found text, we can certainly put the focus in it. Let's just find a correct position
// based on the unit.
if ( value.type == 'text' ) {
return getCorrectPosition( data.walker, data.unit );
}
// Entering an element.
if ( value.type == ( data.isForward ? 'elementStart' : 'elementEnd' ) ) {
// If it's an object, we can select it now.
if ( data.schema.objects.has( value.item.name ) ) {
return position_Position.createAt( value.item, data.isForward ? 'after' : 'before' );
}
// If text allowed on this position, extend to this place.
if ( data.schema.check( { name: '$text', inside: value.nextPosition } ) ) {
return value.nextPosition;
}
}
// Leaving an element.
else {
// If leaving a limit element, stop.
if ( data.schema.limits.has( value.item.name ) ) {
// NOTE: Fast-forward the walker until the end.
data.walker.skip( () => true );
return;
}
// If text allowed on this position, extend to this place.
if ( data.schema.check( { name: '$text', inside: value.nextPosition } ) ) {
return value.nextPosition;
}
}
}
// Finds a correct position by walking in a text node and checking whether selection can be extended to given position
// or should be extended further.
function getCorrectPosition( walker, unit ) {
const textNode = walker.position.textNode;
if ( textNode ) {
const data = textNode.data;
let offset = walker.position.offset - textNode.startOffset;
while ( isInsideSurrogatePair( data, offset ) || ( unit == 'character' && isInsideCombinedSymbol( data, offset ) ) ) {
walker.next();
offset = walker.position.offset - textNode.startOffset;
}
}
return walker.position;
}
function getSearchRange( start, isForward ) {
const root = start.root;
const searchEnd = position_Position.createAt( root, isForward ? 'end' : 0 );
if ( isForward ) {
return new range_Range( start, searchEnd );
} else {
return new range_Range( searchEnd, start );
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/controller/getselectedcontent.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/controller/getselectedcontent
*/
/**
* Gets a clone of the selected content.
*
* For example, for the following selection:
*
* <p>x</p><quote><p>y</p><h>fir[st</h></quote><p>se]cond</p><p>z</p>
*
* It will return a document fragment with such a content:
*
* <quote><h>st</h></quote><p>se</p>
*
* @param {module:engine/model/selection~Selection} selection The selection of which content will be returned.
* @returns {module:engine/model/documentfragment~DocumentFragment}
*/
function getSelectedContent( selection ) {
const frag = new documentfragment_DocumentFragment();
const range = selection.getFirstRange();
if ( !range || range.isCollapsed ) {
return frag;
}
const root = range.start.root;
const commonPath = range.start.getCommonPath( range.end );
const commonParent = root.getNodeByPath( commonPath );
// ## 1st step
//
// First, we'll clone a fragment represented by a minimal flat range
// containing the original range to be cloned.
// E.g. let's consider such a range:
//
// <p>x</p><quote><p>y</p><h>fir[st</h></quote><p>se]cond</p><p>z</p>
//
// A minimal flat range containing this one is:
//
// <p>x</p>[<quote><p>y</p><h>first</h></quote><p>second</p>]<p>z</p>
//
// We can easily clone this structure, preserving e.g. the <quote> element.
let flatSubtreeRange;
if ( range.start.parent == range.end.parent ) {
// The original range is flat, so take it.
flatSubtreeRange = range;
} else {
flatSubtreeRange = range_Range.createFromParentsAndOffsets(
commonParent, range.start.path[ commonPath.length ],
commonParent, range.end.path[ commonPath.length ] + 1
);
}
const howMany = flatSubtreeRange.end.offset - flatSubtreeRange.start.offset;
// Clone the whole contents.
for ( const item of flatSubtreeRange.getItems( { shallow: true } ) ) {
if ( item.is( 'textProxy' ) ) {
frag.appendChildren( new text_Text( item.data, item.getAttributes() ) );
} else {
frag.appendChildren( item.clone( true ) );
}
}
// ## 2nd step
//
// If the original range wasn't flat, then we need to remove the excess nodes from the both ends of the cloned fragment.
//
// For example, for the range shown in the 1st step comment, we need to remove these pieces:
//
// <quote>[<p>y</p>]<h>[fir]st</h></quote><p>se[cond]</p>
//
// So this will be the final copied content:
//
// <quote><h>st</h></quote><p>se</p>
//
// In order to do that, we remove content from these two ranges:
//
// [<quote><p>y</p><h>fir]st</h></quote><p>se[cond</p>]
if ( flatSubtreeRange != range ) {
// Find the position of the original range in the cloned fragment.
const newRange = range._getTransformedByMove( flatSubtreeRange.start, position_Position.createAt( frag, 0 ), howMany )[ 0 ];
const leftExcessRange = new range_Range( position_Position.createAt( frag ), newRange.start );
const rightExcessRange = new range_Range( newRange.end, position_Position.createAt( frag, 'end' ) );
removeRangeContent( rightExcessRange );
removeRangeContent( leftExcessRange );
}
return frag;
}
// After https://github.com/ckeditor/ckeditor5-engine/issues/690 is fixed,
// this function will, most likely, be able to rewritten using getMinimalFlatRanges().
function removeRangeContent( range ) {
const parentsToCheck = [];
Array.from( range.getItems( { direction: 'backward' } ) )
// We should better store ranges because text proxies will lose integrity
// with the text nodes when we'll start removing content.
.map( item => range_Range.createOn( item ) )
// Filter only these items which are fully contained in the passed range.
//
// E.g. for the following range: [<quote><p>y</p><h>fir]st</h>
// the walker will return the entire <h> element, when only the "fir" item inside it is fully contained.
.filter( itemRange => {
// We should be able to use Range.containsRange, but https://github.com/ckeditor/ckeditor5-engine/issues/691.
const contained =
( itemRange.start.isAfter( range.start ) || itemRange.start.isEqual( range.start ) ) &&
( itemRange.end.isBefore( range.end ) || itemRange.end.isEqual( range.end ) );
return contained;
} )
.forEach( itemRange => {
parentsToCheck.push( itemRange.start.parent );
model_writer_remove( itemRange );
} );
// Remove ancestors of the removed items if they turned to be empty now
// (their whole content was contained in the range).
parentsToCheck.forEach( parentToCheck => {
let parent = parentToCheck;
while ( parent.parent && parent.isEmpty ) {
const removeRange = range_Range.createOn( parent );
parent = parent.parent;
model_writer_remove( removeRange );
}
} );
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/controller/datacontroller.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/controller/datacontroller
*/
/**
* Controller for the data pipeline. The data pipeline controls how data is retrieved from the document
* and set inside it. Hence, the controller features two methods which allow to {@link ~DataController#get get}
* and {@link ~DataController#set set} data of the {@link ~DataController#model model}
* using given:
*
* * {@link module:engine/dataprocessor/dataprocessor~DataProcessor data processor},
* * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher model to view} and
* * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher view to model} converters.
*
* @mixes module:utils/emittermixin~ObservableMixin
*/
class datacontroller_DataController {
/**
* Creates data controller instance.
*
* @param {module:engine/model/document~Document} model Document model.
* @param {module:engine/dataprocessor/dataprocessor~DataProcessor} [dataProcessor] Data processor which should used by the controller.
*/
constructor( model, dataProcessor ) {
/**
* Document model.
*
* @readonly
* @member {module:engine/model/document~Document}
*/
this.model = model;
/**
* Data processor used during the conversion.
*
* @readonly
* @member {module:engine/dataProcessor~DataProcessor}
*/
this.processor = dataProcessor;
/**
* Mapper used for the conversion. It has no permanent bindings, because they are created when getting data and
* cleared directly after data are converted. However, the mapper is defined as class property, because
* it needs to be passed to the `ModelConversionDispatcher` as a conversion API.
*
* @member {module:engine/conversion/mapper~Mapper}
*/
this.mapper = new mapper_Mapper();
/**
* Model to view conversion dispatcher used by the {@link #get get method}.
* To attach model to view converter to the data pipeline you need to add lister to this property:
*
* data.modelToView( 'insert:$element', customInsertConverter );
*
* Or use {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder}:
*
* buildModelConverter().for( data.modelToView ).fromAttribute( 'bold' ).toElement( 'b' );
*
* @readonly
* @member {module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}
*/
this.modelToView = new modelconversiondispatcher_ModelConversionDispatcher( this.model, {
mapper: this.mapper
} );
this.modelToView.on( 'insert:$text', model_to_view_converters_insertText(), { priority: 'lowest' } );
/**
* View to model conversion dispatcher used by the {@link #set set method}.
* To attach view to model converter to the data pipeline you need to add lister to this property:
*
* data.viewToModel( 'element', customElementConverter );
*
* Or use {@link module:engine/conversion/buildviewconverter~ViewConverterBuilder}:
*
* buildViewConverter().for( data.viewToModel ).fromElement( 'b' ).toAttribute( 'bold', 'true' );
*
* @readonly
* @member {module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher}
*/
this.viewToModel = new viewconversiondispatcher_ViewConversionDispatcher( {
schema: model.schema
} );
// Define default converters for text and elements.
//
// Note that if there is no default converter for the element it will be skipped, for instance `<b>foo</b>` will be
// converted to nothing. We add `convertToModelFragment` as a last converter so it converts children of that
// element to the document fragment so `<b>foo</b>` will be converted to `foo` if there is no converter for `<b>`.
this.viewToModel.on( 'text', convertText(), { priority: 'lowest' } );
this.viewToModel.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
this.viewToModel.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
[ 'insertContent', 'deleteContent', 'modifySelection', 'getSelectedContent' ]
.forEach( methodName => this.decorate( methodName ) );
}
/**
* Returns model's data converted by the {@link #modelToView model to view converters} and
* formatted by the {@link #processor data processor}.
*
* @param {String} [rootName='main'] Root name.
* @returns {String} Output data.
*/
get( rootName = 'main' ) {
// Get model range.
return this.stringify( this.model.getRoot( rootName ) );
}
/**
* Returns the content of the given {@link module:engine/model/element~Element model's element} or
* {@link module:engine/model/documentfragment~DocumentFragment model document fragment} converted by the
* {@link #modelToView model to view converters} and formatted by the
* {@link #processor data processor}.
*
* @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment
* Element which content will be stringified.
* @returns {String} Output data.
*/
stringify( modelElementOrFragment ) {
// model -> view
const viewDocumentFragment = this.toView( modelElementOrFragment );
// view -> data
return this.processor.toData( viewDocumentFragment );
}
/**
* Returns the content of the given {@link module:engine/model/element~Element model element} or
* {@link module:engine/model/documentfragment~DocumentFragment model document fragment} converted by the
* {@link #modelToView model to view converters} to a
* {@link module:engine/view/documentfragment~DocumentFragment view document fragment}.
*
* @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment
* Element or document fragment which content will be converted.
* @returns {module:engine/view/documentfragment~DocumentFragment} Output view DocumentFragment.
*/
toView( modelElementOrFragment ) {
const modelRange = range_Range.createIn( modelElementOrFragment );
const viewDocumentFragment = new view_documentfragment_DocumentFragment();
this.mapper.bindElements( modelElementOrFragment, viewDocumentFragment );
this.modelToView.convertInsertion( modelRange );
this.mapper.clearBindings();
return viewDocumentFragment;
}
/**
* Sets input data parsed by the {@link #processor data processor} and
* converted by the {@link #viewToModel view to model converters}.
*
* This method also creates a batch with all the changes applied. If all you need is to parse data use
* the {@link #parse} method.
*
* @param {String} data Input data.
* @param {String} [rootName='main'] Root name.
*/
set( data, rootName = 'main' ) {
// Save to model.
const modelRoot = this.model.getRoot( rootName );
this.model.enqueueChanges( () => {
// Clearing selection is a workaround for ticket #569 (LiveRange loses position after removing data from document).
// After fixing it this code should be removed.
this.model.selection.removeAllRanges();
this.model.selection.clearAttributes();
// Initial batch should be ignored by features like undo, etc.
this.model.batch( 'transparent' )
.remove( range_Range.createIn( modelRoot ) )
.insert( position_Position.createAt( modelRoot, 0 ), this.parse( data ) );
} );
}
/**
* Returns data parsed by the {@link #processor data processor} and then
* converted by the {@link #viewToModel view to model converters}.
*
* @see #set
* @param {String} data Data to parse.
* @param {String} [context='$root'] Base context in which the view will be converted to the model. See:
* {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}.
* @returns {module:engine/model/documentfragment~DocumentFragment} Parsed data.
*/
parse( data, context = '$root' ) {
// data -> view
const viewDocumentFragment = this.processor.toView( data );
// view -> model
return this.toModel( viewDocumentFragment, context );
}
/**
* Returns wrapped by {module:engine/model/documentfragment~DocumentFragment} result of the given
* {@link module:engine/view/element~Element view element} or
* {@link module:engine/view/documentfragment~DocumentFragment view document fragment} converted by the
* {@link #viewToModel view to model converters}.
*
* When marker elements were converted during conversion process then will be set as DocumentFragment's
* {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
*
* @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewElementOrFragment
* Element or document fragment which content will be converted.
* @param {String} [context='$root'] Base context in which the view will be converted to the model. See:
* {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}.
* @returns {module:engine/model/documentfragment~DocumentFragment} Output document fragment.
*/
toModel( viewElementOrFragment, context = '$root' ) {
return this.viewToModel.convert( viewElementOrFragment, { context: [ context ] } );
}
/**
* Removes all event listeners set by the DataController.
*/
destroy() {}
/**
* See {@link module:engine/controller/insertcontent.insertContent}.
*
* @fires insertContent
* @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
* @param {module:engine/model/selection~Selection} selection Selection into which the content should be inserted.
* @param {module:engine/model/batch~Batch} [batch] Batch to which deltas will be added. If not specified, then
* changes will be added to a new batch.
*/
insertContent( content, selection, batch ) {
insertContent( this, content, selection, batch );
}
/**
* See {@link module:engine/controller/deletecontent.deleteContent}.
*
* Note: For the sake of predictability, the resulting selection should always be collapsed.
* In cases where a feature wants to modify deleting behavior so selection isn't collapsed
* (e.g. a table feature may want to keep row selection after pressing <kbd>Backspace</kbd>),
* then that behavior should be implemented in the view's listener. At the same time, the table feature
* will need to modify this method's behavior too, e.g. to "delete contents and then collapse
* the selection inside the last selected cell" or "delete the row and collapse selection somewhere near".
* That needs to be done in order to ensure that other features which use `deleteContent()` will work well with tables.
*
* @fires deleteContent
* @param {module:engine/model/selection~Selection} selection Selection of which the content should be deleted.
* @param {module:engine/model/batch~Batch} batch Batch to which deltas will be added.
* @param {Object} options See {@link module:engine/controller/deletecontent~deleteContent}'s options.
*/
deleteContent( selection, batch, options ) {
deleteContent( selection, batch, options );
}
/**
* See {@link module:engine/controller/modifyselection.modifySelection}.
*
* @fires modifySelection
* @param {module:engine/model/selection~Selection} selection The selection to modify.
* @param {Object} options See {@link module:engine/controller/modifyselection~modifySelection}'s options.
*/
modifySelection( selection, options ) {
modifySelection( this, selection, options );
}
/**
* See {@link module:engine/controller/getselectedcontent.getSelectedContent}.
*
* @fires module:engine/controller/datacontroller~DataController#getSelectedContent
* @param {module:engine/model/selection~Selection} selection The selection of which content will be retrieved.
* @returns {module:engine/model/documentfragment~DocumentFragment} Document fragment holding the clone of the selected content.
*/
getSelectedContent( selection ) {
return getSelectedContent( selection );
}
/**
* Checks whether given {@link module:engine/model/range~Range range} or {@link module:engine/model/element~Element element}
* has any content.
*
* Content is any text node or element which is registered in {@link module:engine/model/schema~Schema schema}.
*
* @param {module:engine/model/range~Range|module:engine/model/element~Element} rangeOrElement Range or element to check.
* @returns {Boolean}
*/
hasContent( rangeOrElement ) {
if ( rangeOrElement instanceof element_Element ) {
rangeOrElement = range_Range.createIn( rangeOrElement );
}
if ( rangeOrElement.isCollapsed ) {
return false;
}
for ( const item of rangeOrElement.getItems() ) {
// Remember, `TreeWalker` returns always `textProxy` nodes.
if ( item.is( 'textProxy' ) || this.model.schema.objects.has( item.name ) ) {
return true;
}
}
return false;
}
}
mix( datacontroller_DataController, observablemixin );
/**
* Event fired when {@link #insertContent} method is called.
*
* The {@link #insertContent default action of that method} is implemented as a
* listener to this event so it can be fully customized by the features.
*
* @event insertContent
* @param {Array} args The arguments passed to the original method.
*/
/**
* Event fired when {@link #deleteContent} method is called.
*
* The {@link #deleteContent default action of that method} is implemented as a
* listener to this event so it can be fully customized by the features.
*
* @event deleteContent
* @param {Array} args The arguments passed to the original method.
*/
/**
* Event fired when {@link #modifySelection} method is called.
*
* The {@link #modifySelection default action of that method} is implemented as a
* listener to this event so it can be fully customized by the features.
*
* @event modifySelection
* @param {Array} args The arguments passed to the original method.
*/
/**
* Event fired when {@link #getSelectedContent} method is called.
*
* The {@link #getSelectedContent default action of that method} is implemented as a
* listener to this event so it can be fully customized by the features.
*
* @event getSelectedContent
* @param {Array} args The arguments passed to the original method.
*/
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operation.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/operation/operation
*/
/**
* Abstract base operation class.
*
* @abstract
*/
class operation_Operation {
/**
* Base operation constructor.
* @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which the operation can be applied.
*/
constructor( baseVersion ) {
/**
* {@link module:engine/model/document~Document#version} on which operation can be applied. If you try to
* {@link module:engine/model/document~Document#applyOperation apply} operation with different base version than the
* {@link module:engine/model/document~Document#version document version} the
* {@link module:utils/ckeditorerror~CKEditorError model-document-applyOperation-wrong-version} error is thrown.
*
* @member {Number}
*/
this.baseVersion = baseVersion;
/**
* Operation type.
*
* @readonly
* @member {String} #type
*/
/**
* {@link module:engine/model/delta/delta~Delta Delta} which the operation is a part of. This property is set by the
* {@link module:engine/model/delta/delta~Delta delta} when the operations is added to it by the
* {@link module:engine/model/delta/delta~Delta#addOperation} method.
*
* @member {module:engine/model/delta/delta~Delta} #delta
*/
/**
* Creates and returns an operation that has the same parameters as this operation.
*
* @method #clone
* @returns {module:engine/model/operation/operation~Operation} Clone of this operation.
*/
/**
* Creates and returns a reverse operation. Reverse operation when executed right after
* the original operation will bring back tree model state to the point before the original
* operation execution. In other words, it reverses changes done by the original operation.
*
* Keep in mind that tree model state may change since executing the original operation,
* so reverse operation will be "outdated". In that case you will need to
* {@link module:engine/model/operation/transform~transform} it by all operations that were executed after the original operation.
*
* @method #getReversed
* @returns {module:engine/model/operation/operation~Operation} Reversed operation.
*/
/**
* Executes the operation - modifications described by the operation attributes
* will be applied to the tree model.
*
* @protected
* @method #_execute
* @returns {Object} Object with additional information about the applied changes. It properties depends on the
* operation type.
*/
}
/**
* Custom toJSON method to solve child-parent circular dependencies.
*
* @method #toJSON
* @returns {Object} Clone of this object with the delta property replaced with string.
*/
toJSON() {
const json = lodash_clone( this, true );
json.__className = this.constructor.className;
// Remove parent delta to avoid circular dependencies.
delete json.delta;
return json;
}
/**
* Name of the operation class used for serialization.
*
* @type {String}
*/
static get className() {
return 'engine.model.operation.Operation';
}
/**
* Creates Operation object from deserilized object, i.e. from parsed JSON string.
*
* @param {Object} json Deserialized JSON object.
* @param {module:engine/model/document~Document} doc Document on which this operation will be applied.
* @returns {module:engine/model/operation/operation~Operation}
*/
static fromJSON( json ) {
return new this( json.baseVersion );
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_setCacheAdd.js
/** Used to stand-in for `undefined` hash values. */
var _setCacheAdd_HASH_UNDEFINED = '__lodash_hash_undefined__';
/**
* Adds `value` to the array cache.
*
* @private
* @name add
* @memberOf SetCache
* @alias push
* @param {*} value The value to cache.
* @returns {Object} Returns the cache instance.
*/
function setCacheAdd(value) {
this.__data__.set(value, _setCacheAdd_HASH_UNDEFINED);
return this;
}
/* harmony default export */ var _setCacheAdd = (setCacheAdd);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_setCacheHas.js
/**
* Checks if `value` is in the array cache.
*
* @private
* @name has
* @memberOf SetCache
* @param {*} value The value to search for.
* @returns {number} Returns `true` if `value` is found, else `false`.
*/
function setCacheHas(value) {
return this.__data__.has(value);
}
/* harmony default export */ var _setCacheHas = (setCacheHas);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_SetCache.js
/**
*
* Creates an array cache object to store unique values.
*
* @private
* @constructor
* @param {Array} [values] The values to cache.
*/
function SetCache(values) {
var index = -1,
length = values ? values.length : 0;
this.__data__ = new _MapCache;
while (++index < length) {
this.add(values[index]);
}
}
// Add methods to `SetCache`.
SetCache.prototype.add = SetCache.prototype.push = _setCacheAdd;
SetCache.prototype.has = _setCacheHas;
/* harmony default export */ var _SetCache = (SetCache);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_arraySome.js
/**
* A specialized version of `_.some` for arrays without support for iteratee
* shorthands.
*
* @private
* @param {Array} array The array to iterate over.
* @param {Function} predicate The function invoked per iteration.
* @returns {boolean} Returns `true` if any element passes the predicate check,
* else `false`.
*/
function arraySome(array, predicate) {
var index = -1,
length = array.length;
while (++index < length) {
if (predicate(array[index], index, array)) {
return true;
}
}
return false;
}
/* harmony default export */ var _arraySome = (arraySome);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_equalArrays.js
/** Used to compose bitmasks for comparison styles. */
var UNORDERED_COMPARE_FLAG = 1,
PARTIAL_COMPARE_FLAG = 2;
/**
* A specialized version of `baseIsEqualDeep` for arrays with support for
* partial deep comparisons.
*
* @private
* @param {Array} array The array to compare.
* @param {Array} other The other array to compare.
* @param {Function} equalFunc The function to determine equivalents of values.
* @param {Function} customizer The function to customize comparisons.
* @param {number} bitmask The bitmask of comparison flags. See `baseIsEqual`
* for more details.
* @param {Object} stack Tracks traversed `array` and `other` objects.
* @returns {boolean} Returns `true` if the arrays are equivalent, else `false`.
*/
function equalArrays(array, other, equalFunc, customizer, bitmask, stack) {
var isPartial = bitmask & PARTIAL_COMPARE_FLAG,
arrLength = array.length,
othLength = other.length;
if (arrLength != othLength && !(isPartial && othLength > arrLength)) {
return false;
}
// Assume cyclic values are equal.
var stacked = stack.get(array);
if (stacked) {
return stacked == other;
}
var index = -1,
result = true,
seen = (bitmask & UNORDERED_COMPARE_FLAG) ? new _SetCache : undefined;
stack.set(array, other);
// Ignore non-index properties.
while (++index < arrLength) {
var arrValue = array[index],
othValue = other[index];
if (customizer) {
var compared = isPartial
? customizer(othValue, arrValue, index, other, array, stack)
: customizer(arrValue, othValue, index, array, other, stack);
}
if (compared !== undefined) {
if (compared) {
continue;
}
result = false;
break;
}
// Recursively compare arrays (susceptible to call stack limits).
if (seen) {
if (!_arraySome(other, function(othValue, othIndex) {
if (!seen.has(othIndex) &&
(arrValue === othValue || equalFunc(arrValue, othValue, customizer, bitmask, stack))) {
return seen.add(othIndex);
}
})) {
result = false;
break;
}
} else if (!(
arrValue === othValue ||
equalFunc(arrValue, othValue, customizer, bitmask, stack)
)) {
result = false;
break;
}
}
stack['delete'](array);
return result;
}
/* harmony default export */ var _equalArrays = (equalArrays);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_equalByTag.js
/** Used to compose bitmasks for comparison styles. */
var _equalByTag_UNORDERED_COMPARE_FLAG = 1,
_equalByTag_PARTIAL_COMPARE_FLAG = 2;
/** `Object#toString` result references. */
var _equalByTag_boolTag = '[object Boolean]',
_equalByTag_dateTag = '[object Date]',
_equalByTag_errorTag = '[object Error]',
_equalByTag_mapTag = '[object Map]',
_equalByTag_numberTag = '[object Number]',
_equalByTag_regexpTag = '[object RegExp]',
_equalByTag_setTag = '[object Set]',
_equalByTag_stringTag = '[object String]',
_equalByTag_symbolTag = '[object Symbol]';
var _equalByTag_arrayBufferTag = '[object ArrayBuffer]',
_equalByTag_dataViewTag = '[object DataView]';
/** Used to convert symbols to primitives and strings. */
var _equalByTag_symbolProto = _Symbol ? _Symbol.prototype : undefined,
_equalByTag_symbolValueOf = _equalByTag_symbolProto ? _equalByTag_symbolProto.valueOf : undefined;
/**
* A specialized version of `baseIsEqualDeep` for comparing objects of
* the same `toStringTag`.
*
* **Note:** This function only supports comparing values with tags of
* `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`.
*
* @private
* @param {Object} object The object to compare.
* @param {Object} other The other object to compare.
* @param {string} tag The `toStringTag` of the objects to compare.
* @param {Function} equalFunc The function to determine equivalents of values.
* @param {Function} customizer The function to customize comparisons.
* @param {number} bitmask The bitmask of comparison flags. See `baseIsEqual`
* for more details.
* @param {Object} stack Tracks traversed `object` and `other` objects.
* @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
*/
function equalByTag(object, other, tag, equalFunc, customizer, bitmask, stack) {
switch (tag) {
case _equalByTag_dataViewTag:
if ((object.byteLength != other.byteLength) ||
(object.byteOffset != other.byteOffset)) {
return false;
}
object = object.buffer;
other = other.buffer;
case _equalByTag_arrayBufferTag:
if ((object.byteLength != other.byteLength) ||
!equalFunc(new _Uint8Array(object), new _Uint8Array(other))) {
return false;
}
return true;
case _equalByTag_boolTag:
case _equalByTag_dateTag:
// Coerce dates and booleans to numbers, dates to milliseconds and
// booleans to `1` or `0` treating invalid dates coerced to `NaN` as
// not equal.
return +object == +other;
case _equalByTag_errorTag:
return object.name == other.name && object.message == other.message;
case _equalByTag_numberTag:
// Treat `NaN` vs. `NaN` as equal.
return (object != +object) ? other != +other : object == +other;
case _equalByTag_regexpTag:
case _equalByTag_stringTag:
// Coerce regexes to strings and treat strings, primitives and objects,
// as equal. See http://www.ecma-international.org/ecma-262/6.0/#sec-regexp.prototype.tostring
// for more details.
return object == (other + '');
case _equalByTag_mapTag:
var convert = _mapToArray;
case _equalByTag_setTag:
var isPartial = bitmask & _equalByTag_PARTIAL_COMPARE_FLAG;
convert || (convert = _setToArray);
if (object.size != other.size && !isPartial) {
return false;
}
// Assume cyclic values are equal.
var stacked = stack.get(object);
if (stacked) {
return stacked == other;
}
bitmask |= _equalByTag_UNORDERED_COMPARE_FLAG;
stack.set(object, other);
// Recursively compare objects (susceptible to call stack limits).
return _equalArrays(convert(object), convert(other), equalFunc, customizer, bitmask, stack);
case _equalByTag_symbolTag:
if (_equalByTag_symbolValueOf) {
return _equalByTag_symbolValueOf.call(object) == _equalByTag_symbolValueOf.call(other);
}
}
return false;
}
/* harmony default export */ var _equalByTag = (equalByTag);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_equalObjects.js
/** Used to compose bitmasks for comparison styles. */
var _equalObjects_PARTIAL_COMPARE_FLAG = 2;
/**
* A specialized version of `baseIsEqualDeep` for objects with support for
* partial deep comparisons.
*
* @private
* @param {Object} object The object to compare.
* @param {Object} other The other object to compare.
* @param {Function} equalFunc The function to determine equivalents of values.
* @param {Function} customizer The function to customize comparisons.
* @param {number} bitmask The bitmask of comparison flags. See `baseIsEqual`
* for more details.
* @param {Object} stack Tracks traversed `object` and `other` objects.
* @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
*/
function equalObjects(object, other, equalFunc, customizer, bitmask, stack) {
var isPartial = bitmask & _equalObjects_PARTIAL_COMPARE_FLAG,
objProps = lodash_keys(object),
objLength = objProps.length,
othProps = lodash_keys(other),
othLength = othProps.length;
if (objLength != othLength && !isPartial) {
return false;
}
var index = objLength;
while (index--) {
var key = objProps[index];
if (!(isPartial ? key in other : _baseHas(other, key))) {
return false;
}
}
// Assume cyclic values are equal.
var stacked = stack.get(object);
if (stacked) {
return stacked == other;
}
var result = true;
stack.set(object, other);
var skipCtor = isPartial;
while (++index < objLength) {
key = objProps[index];
var objValue = object[key],
othValue = other[key];
if (customizer) {
var compared = isPartial
? customizer(othValue, objValue, key, other, object, stack)
: customizer(objValue, othValue, key, object, other, stack);
}
// Recursively compare objects (susceptible to call stack limits).
if (!(compared === undefined
? (objValue === othValue || equalFunc(objValue, othValue, customizer, bitmask, stack))
: compared
)) {
result = false;
break;
}
skipCtor || (skipCtor = key == 'constructor');
}
if (result && !skipCtor) {
var objCtor = object.constructor,
othCtor = other.constructor;
// Non `Object` object instances with different constructors are not equal.
if (objCtor != othCtor &&
('constructor' in object && 'constructor' in other) &&
!(typeof objCtor == 'function' && objCtor instanceof objCtor &&
typeof othCtor == 'function' && othCtor instanceof othCtor)) {
result = false;
}
}
stack['delete'](object);
return result;
}
/* harmony default export */ var _equalObjects = (equalObjects);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isTypedArray.js
/** `Object#toString` result references. */
var isTypedArray_argsTag = '[object Arguments]',
isTypedArray_arrayTag = '[object Array]',
isTypedArray_boolTag = '[object Boolean]',
isTypedArray_dateTag = '[object Date]',
isTypedArray_errorTag = '[object Error]',
isTypedArray_funcTag = '[object Function]',
isTypedArray_mapTag = '[object Map]',
isTypedArray_numberTag = '[object Number]',
isTypedArray_objectTag = '[object Object]',
isTypedArray_regexpTag = '[object RegExp]',
isTypedArray_setTag = '[object Set]',
isTypedArray_stringTag = '[object String]',
isTypedArray_weakMapTag = '[object WeakMap]';
var isTypedArray_arrayBufferTag = '[object ArrayBuffer]',
isTypedArray_dataViewTag = '[object DataView]',
isTypedArray_float32Tag = '[object Float32Array]',
isTypedArray_float64Tag = '[object Float64Array]',
isTypedArray_int8Tag = '[object Int8Array]',
isTypedArray_int16Tag = '[object Int16Array]',
isTypedArray_int32Tag = '[object Int32Array]',
isTypedArray_uint8Tag = '[object Uint8Array]',
isTypedArray_uint8ClampedTag = '[object Uint8ClampedArray]',
isTypedArray_uint16Tag = '[object Uint16Array]',
isTypedArray_uint32Tag = '[object Uint32Array]';
/** Used to identify `toStringTag` values of typed arrays. */
var typedArrayTags = {};
typedArrayTags[isTypedArray_float32Tag] = typedArrayTags[isTypedArray_float64Tag] =
typedArrayTags[isTypedArray_int8Tag] = typedArrayTags[isTypedArray_int16Tag] =
typedArrayTags[isTypedArray_int32Tag] = typedArrayTags[isTypedArray_uint8Tag] =
typedArrayTags[isTypedArray_uint8ClampedTag] = typedArrayTags[isTypedArray_uint16Tag] =
typedArrayTags[isTypedArray_uint32Tag] = true;
typedArrayTags[isTypedArray_argsTag] = typedArrayTags[isTypedArray_arrayTag] =
typedArrayTags[isTypedArray_arrayBufferTag] = typedArrayTags[isTypedArray_boolTag] =
typedArrayTags[isTypedArray_dataViewTag] = typedArrayTags[isTypedArray_dateTag] =
typedArrayTags[isTypedArray_errorTag] = typedArrayTags[isTypedArray_funcTag] =
typedArrayTags[isTypedArray_mapTag] = typedArrayTags[isTypedArray_numberTag] =
typedArrayTags[isTypedArray_objectTag] = typedArrayTags[isTypedArray_regexpTag] =
typedArrayTags[isTypedArray_setTag] = typedArrayTags[isTypedArray_stringTag] =
typedArrayTags[isTypedArray_weakMapTag] = false;
/** Used for built-in method references. */
var isTypedArray_objectProto = Object.prototype;
/**
* Used to resolve the
* [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring)
* of values.
*/
var isTypedArray_objectToString = isTypedArray_objectProto.toString;
/**
* Checks if `value` is classified as a typed array.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is correctly classified,
* else `false`.
* @example
*
* _.isTypedArray(new Uint8Array);
* // => true
*
* _.isTypedArray([]);
* // => false
*/
function isTypedArray(value) {
return lodash_isObjectLike(value) &&
lodash_isLength(value.length) && !!typedArrayTags[isTypedArray_objectToString.call(value)];
}
/* harmony default export */ var lodash_isTypedArray = (isTypedArray);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseIsEqualDeep.js
/** Used to compose bitmasks for comparison styles. */
var _baseIsEqualDeep_PARTIAL_COMPARE_FLAG = 2;
/** `Object#toString` result references. */
var _baseIsEqualDeep_argsTag = '[object Arguments]',
_baseIsEqualDeep_arrayTag = '[object Array]',
_baseIsEqualDeep_objectTag = '[object Object]';
/** Used for built-in method references. */
var _baseIsEqualDeep_objectProto = Object.prototype;
/** Used to check objects for own properties. */
var _baseIsEqualDeep_hasOwnProperty = _baseIsEqualDeep_objectProto.hasOwnProperty;
/**
* A specialized version of `baseIsEqual` for arrays and objects which performs
* deep comparisons and tracks traversed objects enabling objects with circular
* references to be compared.
*
* @private
* @param {Object} object The object to compare.
* @param {Object} other The other object to compare.
* @param {Function} equalFunc The function to determine equivalents of values.
* @param {Function} [customizer] The function to customize comparisons.
* @param {number} [bitmask] The bitmask of comparison flags. See `baseIsEqual`
* for more details.
* @param {Object} [stack] Tracks traversed `object` and `other` objects.
* @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
*/
function baseIsEqualDeep(object, other, equalFunc, customizer, bitmask, stack) {
var objIsArr = lodash_isArray(object),
othIsArr = lodash_isArray(other),
objTag = _baseIsEqualDeep_arrayTag,
othTag = _baseIsEqualDeep_arrayTag;
if (!objIsArr) {
objTag = _getTag(object);
objTag = objTag == _baseIsEqualDeep_argsTag ? _baseIsEqualDeep_objectTag : objTag;
}
if (!othIsArr) {
othTag = _getTag(other);
othTag = othTag == _baseIsEqualDeep_argsTag ? _baseIsEqualDeep_objectTag : othTag;
}
var objIsObj = objTag == _baseIsEqualDeep_objectTag && !_isHostObject(object),
othIsObj = othTag == _baseIsEqualDeep_objectTag && !_isHostObject(other),
isSameTag = objTag == othTag;
if (isSameTag && !objIsObj) {
stack || (stack = new _Stack);
return (objIsArr || lodash_isTypedArray(object))
? _equalArrays(object, other, equalFunc, customizer, bitmask, stack)
: _equalByTag(object, other, objTag, equalFunc, customizer, bitmask, stack);
}
if (!(bitmask & _baseIsEqualDeep_PARTIAL_COMPARE_FLAG)) {
var objIsWrapped = objIsObj && _baseIsEqualDeep_hasOwnProperty.call(object, '__wrapped__'),
othIsWrapped = othIsObj && _baseIsEqualDeep_hasOwnProperty.call(other, '__wrapped__');
if (objIsWrapped || othIsWrapped) {
var objUnwrapped = objIsWrapped ? object.value() : object,
othUnwrapped = othIsWrapped ? other.value() : other;
stack || (stack = new _Stack);
return equalFunc(objUnwrapped, othUnwrapped, customizer, bitmask, stack);
}
}
if (!isSameTag) {
return false;
}
stack || (stack = new _Stack);
return _equalObjects(object, other, equalFunc, customizer, bitmask, stack);
}
/* harmony default export */ var _baseIsEqualDeep = (baseIsEqualDeep);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseIsEqual.js
/**
* The base implementation of `_.isEqual` which supports partial comparisons
* and tracks traversed objects.
*
* @private
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @param {Function} [customizer] The function to customize comparisons.
* @param {boolean} [bitmask] The bitmask of comparison flags.
* The bitmask may be composed of the following flags:
* 1 - Unordered comparison
* 2 - Partial comparison
* @param {Object} [stack] Tracks traversed `value` and `other` objects.
* @returns {boolean} Returns `true` if the values are equivalent, else `false`.
*/
function baseIsEqual(value, other, customizer, bitmask, stack) {
if (value === other) {
return true;
}
if (value == null || other == null || (!lodash_isObject(value) && !lodash_isObjectLike(other))) {
return value !== value && other !== other;
}
return _baseIsEqualDeep(value, other, baseIsEqual, customizer, bitmask, stack);
}
/* harmony default export */ var _baseIsEqual = (baseIsEqual);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isEqual.js
/**
* Performs a deep comparison between two values to determine if they are
* equivalent.
*
* **Note:** This method supports comparing arrays, array buffers, booleans,
* date objects, error objects, maps, numbers, `Object` objects, regexes,
* sets, strings, symbols, and typed arrays. `Object` objects are compared
* by their own, not inherited, enumerable properties. Functions and DOM
* nodes are **not** supported.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @returns {boolean} Returns `true` if the values are equivalent,
* else `false`.
* @example
*
* var object = { 'user': 'fred' };
* var other = { 'user': 'fred' };
*
* _.isEqual(object, other);
* // => true
*
* object === other;
* // => false
*/
function isEqual(value, other) {
return _baseIsEqual(value, other);
}
/* harmony default export */ var lodash_isEqual = (isEqual);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/attributeoperation.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/operation/attributeoperation
*/
/**
* Operation to change nodes' attribute.
*
* Using this class you can add, remove or change value of the attribute.
*
* @extends module:engine/model/operation/operation~Operation
*/
class attributeoperation_AttributeOperation extends operation_Operation {
/**
* Creates an operation that changes, removes or adds attributes.
*
* If only `newValue` is set, attribute will be added on a node. Note that all nodes in operation's range must not
* have an attribute with the same key as the added attribute.
*
* If only `oldValue` is set, then attribute with given key will be removed. Note that all nodes in operation's range
* must have an attribute with that key added.
*
* If both `newValue` and `oldValue` are set, then the operation will change the attribute value. Note that all nodes in
* operation's ranges must already have an attribute with given key and `oldValue` as value
*
* @param {module:engine/model/range~Range} range Range on which the operation should be applied.
* @param {String} key Key of an attribute to change or remove.
* @param {*} oldValue Old value of the attribute with given key or `null`, if attribute was not set before.
* @param {*} newValue New value of the attribute with given key or `null`, if operation should remove attribute.
* @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which the operation can be applied.
*/
constructor( range, key, oldValue, newValue, baseVersion ) {
super( baseVersion );
/**
* Range on which operation should be applied.
*
* @readonly
* @member {module:engine/model/range~Range}
*/
this.range = range_Range.createFromRange( range );
/**
* Key of an attribute to change or remove.
*
* @readonly
* @member {String}
*/
this.key = key;
/**
* Old value of the attribute with given key or `null`, if attribute was not set before.
*
* @readonly
* @member {*}
*/
this.oldValue = oldValue === undefined ? null : oldValue;
/**
* New value of the attribute with given key or `null`, if operation should remove attribute.
*
* @readonly
* @member {*}
*/
this.newValue = newValue === undefined ? null : newValue;
}
/**
* @inheritDoc
*/
get type() {
if ( this.oldValue === null ) {
return 'addAttribute';
} else if ( this.newValue === null ) {
return 'removeAttribute';
} else {
return 'changeAttribute';
}
}
/**
* Creates and returns an operation that has the same parameters as this operation.
*
* @returns {module:engine/model/operation/attributeoperation~AttributeOperation} Clone of this operation.
*/
clone() {
return new attributeoperation_AttributeOperation( this.range, this.key, this.oldValue, this.newValue, this.baseVersion );
}
/**
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
*
* @returns {module:engine/model/operation/attributeoperation~AttributeOperation}
*/
getReversed() {
return new attributeoperation_AttributeOperation( this.range, this.key, this.newValue, this.oldValue, this.baseVersion + 1 );
}
/**
* @inheritDoc
*/
_execute() {
// Validation.
for ( const item of this.range.getItems() ) {
if ( this.oldValue !== null && !lodash_isEqual( item.getAttribute( this.key ), this.oldValue ) ) {
/**
* Changed node has different attribute value than operation's old attribute value.
*
* @error operation-attribute-wrong-old-value
* @param {module:engine/model/item~Item} item
* @param {String} key
* @param {*} value
*/
throw new CKEditorError(
'attribute-operation-wrong-old-value: Changed node has different attribute value than operation\'s ' +
'old attribute value.',
{ item, key: this.key, value: this.oldValue }
);
}
if ( this.oldValue === null && this.newValue !== null && item.hasAttribute( this.key ) ) {
/**
* The attribute with given key already exists for the given node.
*
* @error attribute-operation-attribute-exists
* @param {module:engine/model/node~Node} node
* @param {String} key
*/
throw new CKEditorError(
'attribute-operation-attribute-exists: The attribute with given key already exists.',
{ node: item, key: this.key }
);
}
}
// If value to set is same as old value, don't do anything.
if ( !lodash_isEqual( this.oldValue, this.newValue ) ) {
// Execution.
model_writer.setAttribute( this.range, this.key, this.newValue );
}
return { range: this.range, key: this.key, oldValue: this.oldValue, newValue: this.newValue };
}
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.operation.AttributeOperation';
}
/**
* Creates `AttributeOperation` object from deserilized object, i.e. from parsed JSON string.
*
* @param {Object} json Deserialized JSON object.
* @param {module:engine/model/document~Document} document Document on which this operation will be applied.
* @returns {module:engine/model/operation/attributeoperation~AttributeOperation}
*/
static fromJSON( json, document ) {
return new attributeoperation_AttributeOperation( range_Range.fromJSON( json.range, document ), json.key, json.oldValue, json.newValue, json.baseVersion );
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/moveoperation.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/operation/moveoperation
*/
/**
* Operation to move a range of {@link module:engine/model/item~Item model items}
* to given {@link module:engine/model/position~Position target position}.
*
* @extends module:engine/model/operation/operation~Operation
*/
class moveoperation_MoveOperation extends operation_Operation {
/**
* Creates a move operation.
*
* @param {module:engine/model/position~Position} sourcePosition
* Position before the first {@link module:engine/model/item~Item model item} to move.
* @param {Number} howMany Offset size of moved range. Moved range will start from `sourcePosition` and end at
* `sourcePosition` with offset shifted by `howMany`.
* @param {module:engine/model/position~Position} targetPosition Position at which moved nodes will be inserted.
* @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which operation can be applied.
*/
constructor( sourcePosition, howMany, targetPosition, baseVersion ) {
super( baseVersion );
/**
* Position before the first {@link module:engine/model/item~Item model item} to move.
*
* @member {module:engine/model/position~Position} module:engine/model/operation/moveoperation~MoveOperation#sourcePosition
*/
this.sourcePosition = position_Position.createFromPosition( sourcePosition );
/**
* Offset size of moved range.
*
* @member {Number} module:engine/model/operation/moveoperation~MoveOperation#howMany
*/
this.howMany = howMany;
/**
* Position at which moved nodes will be inserted.
*
* @member {module:engine/model/position~Position} module:engine/model/operation/moveoperation~MoveOperation#targetPosition
*/
this.targetPosition = position_Position.createFromPosition( targetPosition );
/**
* Defines whether `MoveOperation` is sticky. If `MoveOperation` is sticky, during
* {@link module:engine/model/operation/transform~transform operational transformation} if there will be an operation that
* inserts some nodes at the position equal to the boundary of this `MoveOperation`, that operation will
* get their insertion path updated to the position where this `MoveOperation` moves the range.
*
* @member {Boolean} module:engine/model/operation/moveoperation~MoveOperation#isSticky
*/
this.isSticky = false;
}
/**
* @inheritDoc
*/
get type() {
return 'move';
}
/**
* Creates and returns an operation that has the same parameters as this operation.
*
* @returns {module:engine/model/operation/moveoperation~MoveOperation} Clone of this operation.
*/
clone() {
const op = new this.constructor( this.sourcePosition, this.howMany, this.targetPosition, this.baseVersion );
op.isSticky = this.isSticky;
return op;
}
/**
* Returns the start position of the moved range after it got moved. This may be different than
* {@link module:engine/model/operation/moveoperation~MoveOperation#targetPosition} in some cases, i.e. when a range is moved
* inside the same parent but {@link module:engine/model/operation/moveoperation~MoveOperation#targetPosition targetPosition}
* is after {@link module:engine/model/operation/moveoperation~MoveOperation#sourcePosition sourcePosition}.
*
* vv vv
* abcdefg ===> adefbcg
* ^ ^
* targetPos movedRangeStart
* offset 6 offset 4
*
* @returns {module:engine/model/position~Position}
*/
getMovedRangeStart() {
return this.targetPosition._getTransformedByDeletion( this.sourcePosition, this.howMany );
}
/**
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
*
* @returns {module:engine/model/operation/moveoperation~MoveOperation}
*/
getReversed() {
const newTargetPosition = this.sourcePosition._getTransformedByInsertion( this.targetPosition, this.howMany );
const op = new this.constructor( this.getMovedRangeStart(), this.howMany, newTargetPosition, this.baseVersion + 1 );
op.isSticky = this.isSticky;
return op;
}
/**
* @inheritDoc
*/
_execute() {
const sourceElement = this.sourcePosition.parent;
const targetElement = this.targetPosition.parent;
const sourceOffset = this.sourcePosition.offset;
const targetOffset = this.targetPosition.offset;
// Validate whether move operation has correct parameters.
// Validation is pretty complex but move operation is one of the core ways to manipulate the document state.
// We expect that many errors might be connected with one of scenarios described below.
if ( !sourceElement || !targetElement ) {
/**
* Source position or target position is invalid.
*
* @error move-operation-position-invalid
*/
throw new CKEditorError(
'move-operation-position-invalid: Source position or target position is invalid.'
);
} else if ( sourceOffset + this.howMany > sourceElement.maxOffset ) {
/**
* The nodes which should be moved do not exist.
*
* @error move-operation-nodes-do-not-exist
*/
throw new CKEditorError(
'move-operation-nodes-do-not-exist: The nodes which should be moved do not exist.'
);
} else if ( sourceElement === targetElement && sourceOffset < targetOffset && targetOffset < sourceOffset + this.howMany ) {
/**
* Trying to move a range of nodes into the middle of that range.
*
* @error move-operation-range-into-itself
*/
throw new CKEditorError(
'move-operation-range-into-itself: Trying to move a range of nodes to the inside of that range.'
);
} else if ( this.sourcePosition.root == this.targetPosition.root ) {
if ( compareArrays( this.sourcePosition.getParentPath(), this.targetPosition.getParentPath() ) == 'prefix' ) {
const i = this.sourcePosition.path.length - 1;
if ( this.targetPosition.path[ i ] >= sourceOffset && this.targetPosition.path[ i ] < sourceOffset + this.howMany ) {
/**
* Trying to move a range of nodes into one of nodes from that range.
*
* @error move-operation-node-into-itself
*/
throw new CKEditorError(
'move-operation-node-into-itself: Trying to move a range of nodes into one of nodes from that range.'
);
}
}
}
const range = model_writer.move( range_Range.createFromPositionAndShift( this.sourcePosition, this.howMany ), this.targetPosition );
return {
sourcePosition: this.sourcePosition,
range
};
}
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.operation.MoveOperation';
}
/**
* Creates `MoveOperation` object from deserilized object, i.e. from parsed JSON string.
*
* @param {Object} json Deserialized JSON object.
* @param {module:engine/model/document~Document} document Document on which this operation will be applied.
* @returns {module:engine/model/operation/moveoperation~MoveOperation}
*/
static fromJSON( json, document ) {
const sourcePosition = position_Position.fromJSON( json.sourcePosition, document );
const targetPosition = position_Position.fromJSON( json.targetPosition, document );
const move = new this( sourcePosition, json.howMany, targetPosition, json.baseVersion );
if ( json.isSticky ) {
move.isSticky = true;
}
return move;
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/reinsertoperation.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/operation/reinsertoperation
*/
/**
* Operation to reinsert previously removed nodes back to the non-graveyard root. This operation acts like
* {@link module:engine/model/operation/moveoperation~MoveOperation} but it returns
* {@link module:engine/model/operation/removeoperation~RemoveOperation} when reversed
* and fires different change event.
*/
class reinsertoperation_ReinsertOperation extends moveoperation_MoveOperation {
/**
* Position where nodes will be re-inserted.
*
* @type {module:engine/model/position~Position}
*/
get position() {
return this.targetPosition;
}
/**
* @param {module:engine/model/position~Position} pos
*/
set position( pos ) {
this.targetPosition = pos;
}
/**
* @inheritDoc
*/
get type() {
return 'reinsert';
}
/**
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
*
* @returns {module:engine/model/operation/removeoperation~RemoveOperation}
*/
getReversed() {
const newTargetPosition = this.sourcePosition._getTransformedByInsertion( this.targetPosition, this.howMany );
return new removeoperation_RemoveOperation( this.getMovedRangeStart(), this.howMany, newTargetPosition, this.baseVersion + 1 );
}
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.operation.ReinsertOperation';
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/removeoperation.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/operation/removeoperation
*/
/**
* Operation to remove a range of nodes.
*/
class removeoperation_RemoveOperation extends moveoperation_MoveOperation {
/**
* @inheritDoc
*/
get type() {
return 'remove';
}
/**
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
*
* @returns {module:engine/model/operation/reinsertoperation~ReinsertOperation|module:engine/model/operation/nooperation~NoOperation}
*/
getReversed() {
const newTargetPosition = this.sourcePosition._getTransformedByInsertion( this.targetPosition, this.howMany );
return new reinsertoperation_ReinsertOperation( this.getMovedRangeStart(), this.howMany, newTargetPosition, this.baseVersion + 1 );
}
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.operation.RemoveOperation';
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/insertoperation.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/operation/insertoperation
*/
/**
* Operation to insert one or more nodes at given position in the model.
*
* @extends module:engine/model/operation/operation~Operation
*/
class insertoperation_InsertOperation extends operation_Operation {
/**
* Creates an insert operation.
*
* @param {module:engine/model/position~Position} position Position of insertion.
* @param {module:engine/model/node~NodeSet} nodes The list of nodes to be inserted.
* @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which operation can be applied.
*/
constructor( position, nodes, baseVersion ) {
super( baseVersion );
/**
* Position of insertion.
*
* @readonly
* @member {module:engine/model/position~Position} module:engine/model/operation/insertoperation~InsertOperation#position
*/
this.position = position_Position.createFromPosition( position );
/**
* List of nodes to insert.
*
* @readonly
* @member {module:engine/model/nodelist~NodeList} module:engine/model/operation/insertoperation~InsertOperation#nodeList
*/
this.nodes = new nodelist_NodeList( normalizeNodes( nodes ) );
}
/**
* @inheritDoc
*/
get type() {
return 'insert';
}
/**
* Creates and returns an operation that has the same parameters as this operation.
*
* @returns {module:engine/model/operation/insertoperation~InsertOperation} Clone of this operation.
*/
clone() {
const nodes = new nodelist_NodeList( [ ...this.nodes ].map( node => node.clone( true ) ) );
return new insertoperation_InsertOperation( this.position, nodes, this.baseVersion );
}
/**
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
*
* @returns {module:engine/model/operation/removeoperation~RemoveOperation}
*/
getReversed() {
const graveyard = this.position.root.document.graveyard;
const gyPosition = new position_Position( graveyard, [ 0 ] );
return new removeoperation_RemoveOperation( this.position, this.nodes.maxOffset, gyPosition, this.baseVersion + 1 );
}
/**
* @inheritDoc
*/
_execute() {
// What happens here is that we want original nodes be passed to writer because we want original nodes
// to be inserted to the model. But in InsertOperation, we want to keep those nodes as they were added
// to the operation, not modified. For example, text nodes can get merged or cropped while Elements can
// get children. It is important that InsertOperation has the copy of original nodes in intact state.
const originalNodes = this.nodes;
this.nodes = new nodelist_NodeList( [ ...originalNodes ].map( node => node.clone( true ) ) );
const range = model_writer_insert( this.position, originalNodes );
return { range };
}
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.operation.InsertOperation';
}
/**
* Creates `InsertOperation` object from deserilized object, i.e. from parsed JSON string.
*
* @param {Object} json Deserialized JSON object.
* @param {module:engine/model/document~Document} document Document on which this operation will be applied.
* @returns {module:engine/model/operation/insertoperation~InsertOperation}
*/
static fromJSON( json, document ) {
const children = [];
for ( const child of json.nodes ) {
if ( child.name ) {
// If child has name property, it is an Element.
children.push( element_Element.fromJSON( child ) );
} else {
// Otherwise, it is a Text node.
children.push( text_Text.fromJSON( child ) );
}
}
return new insertoperation_InsertOperation( position_Position.fromJSON( json.position, document ), children, json.baseVersion );
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/markeroperation.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/operation/markeroperation
*/
/**
* @extends module:engine/model/operation/operation~Operation
*/
class markeroperation_MarkerOperation extends operation_Operation {
/**
* @param {String} name Marker name.
* @param {module:engine/model/range~Range} oldRange Marker range before the change.
* @param {module:engine/model/range~Range} newRange Marker range after the change.
* @param {module:engine/model/markercollection~MarkerCollection} markers Marker collection on which change should be executed.
* @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which the operation can be applied.
*/
constructor( name, oldRange, newRange, markers, baseVersion ) {
super( baseVersion );
/**
* Marker name.
*
* @readonly
* @member {String}
*/
this.name = name;
/**
* Marker range before the change.
*
* @readonly
* @member {module:engine/model/range~Range}
*/
this.oldRange = oldRange ? range_Range.createFromRange( oldRange ) : null;
/**
* Marker range after the change.
*
* @readonly
* @member {module:engine/model/range~Range}
*/
this.newRange = newRange ? range_Range.createFromRange( newRange ) : null;
/**
* Marker collection on which change should be executed.
*
* @private
* @member {module:engine/model/markercollection~MarkerCollection}
*/
this._markers = markers;
}
/**
* @inheritDoc
*/
get type() {
return 'marker';
}
/**
* Creates and returns an operation that has the same parameters as this operation.
*
* @returns {module:engine/model/operation/markeroperation~MarkerOperation} Clone of this operation.
*/
clone() {
return new markeroperation_MarkerOperation( this.name, this.oldRange, this.newRange, this._markers, this.baseVersion );
}
/**
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
*
* @returns {module:engine/model/operation/markeroperation~MarkerOperation}
*/
getReversed() {
return new markeroperation_MarkerOperation( this.name, this.newRange, this.oldRange, this._markers, this.baseVersion + 1 );
}
/**
* @inheritDoc
*/
_execute() {
const type = this.newRange ? 'set' : 'remove';
this._markers[ type ]( this.name, this.newRange );
return { name: this.name, type };
}
/**
* @inheritDoc
*/
toJSON() {
const json = super.toJSON();
delete json._markers;
return json;
}
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.operation.MarkerOperation';
}
/**
* Creates `MarkerOperation` object from deserilized object, i.e. from parsed JSON string.
*
* @param {Object} json Deserialized JSON object.
* @param {module:engine/model/document~Document} document Document on which this operation will be applied.
* @returns {module:engine/model/operation/markeroperation~MarkerOperation}
*/
static fromJSON( json, document ) {
return new markeroperation_MarkerOperation(
json.name,
json.oldRange ? range_Range.fromJSON( json.oldRange, document ) : null,
json.newRange ? range_Range.fromJSON( json.newRange, document ) : null,
document.markers,
json.baseVersion
);
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/nooperation.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/operation/nooperation
*/
/**
* Operation which is doing nothing ("empty operation", "do-nothing operation", "noop"). This is an operation,
* which when executed does not change the tree model. It still has some parameters defined for transformation purposes.
*
* In most cases this operation is a result of transforming operations. When transformation returns
* {@link module:engine/model/operation/nooperation~NoOperation} it means that changes done by the transformed operation
* have already been applied.
*
* @extends module:engine/model/operation/operation~Operation
*/
class NoOperation extends operation_Operation {
get type() {
return 'noop';
}
/**
* Creates and returns an operation that has the same parameters as this operation.
*
* @returns {module:engine/model/operation/nooperation~NoOperation} Clone of this operation.
*/
clone() {
return new NoOperation( this.baseVersion );
}
/**
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
*
* @returns {module:engine/model/operation/nooperation~NoOperation}
*/
getReversed() {
return new NoOperation( this.baseVersion + 1 );
}
/**
* @inheritDoc
*/
_execute() {
return {};
}
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.operation.NoOperation';
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/renameoperation.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/operation/renameoperation
*/
/**
* Operation to change element's name.
*
* Using this class you can change element's name.
*
* @extends module:engine/model/operation/operation~Operation
*/
class renameoperation_RenameOperation extends operation_Operation {
/**
* Creates an operation that changes element's name.
*
* @param {module:engine/model/position~Position} position Position before an element to change.
* @param {String} oldName Current name of the element.
* @param {String} newName New name for the element.
* @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which the operation can be applied.
*/
constructor( position, oldName, newName, baseVersion ) {
super( baseVersion );
/**
* Position before an element to change.
*
* @member {module:engine/model/position~Position} module:engine/model/operation/renameoperation~RenameOperation#position
*/
this.position = position;
/**
* Current name of the element.
*
* @member {String} module:engine/model/operation/renameoperation~RenameOperation#oldName
*/
this.oldName = oldName;
/**
* New name for the element.
*
* @member {String} module:engine/model/operation/renameoperation~RenameOperation#newName
*/
this.newName = newName;
}
/**
* @inheritDoc
*/
get type() {
return 'rename';
}
/**
* Creates and returns an operation that has the same parameters as this operation.
*
* @returns {module:engine/model/operation/renameoperation~RenameOperation} Clone of this operation.
*/
clone() {
return new renameoperation_RenameOperation( position_Position.createFromPosition( this.position ), this.oldName, this.newName, this.baseVersion );
}
/**
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
*
* @returns {module:engine/model/operation/renameoperation~RenameOperation}
*/
getReversed() {
return new renameoperation_RenameOperation( position_Position.createFromPosition( this.position ), this.newName, this.oldName, this.baseVersion + 1 );
}
/**
* @inheritDoc
*/
_execute() {
// Validation.
const element = this.position.nodeAfter;
if ( !( element instanceof element_Element ) ) {
/**
* Given position is invalid or node after it is not instance of Element.
*
* @error rename-operation-wrong-position
*/
throw new CKEditorError(
'rename-operation-wrong-position: Given position is invalid or node after it is not an instance of Element.'
);
} else if ( element.name !== this.oldName ) {
/**
* Element to change has different name than operation's old name.
*
* @error rename-operation-wrong-name
*/
throw new CKEditorError(
'rename-operation-wrong-name: Element to change has different name than operation\'s old name.'
);
}
// If value to set is same as old value, don't do anything.
if ( element.name != this.newName ) {
// Execution.
element.name = this.newName;
}
return { element, oldName: this.oldName };
}
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.operation.RenameOperation';
}
/**
* Creates `RenameOperation` object from deserialized object, i.e. from parsed JSON string.
*
* @param {Object} json Deserialized JSON object.
* @param {module:engine/model/document~Document} document Document on which this operation will be applied.
* @returns {module:engine/model/operation/attributeoperation~AttributeOperation}
*/
static fromJSON( json, document ) {
return new renameoperation_RenameOperation( position_Position.fromJSON( json.position, document ), json.oldName, json.newName, json.baseVersion );
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/rootattributeoperation.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/operation/rootattributeoperation
*/
/**
* Operation to change root element's attribute. Using this class you can add, remove or change value of the attribute.
*
* This operation is needed, because root elements can't be changed through
* @link module:engine/model/operation/attributeoperation~AttributeOperation}.
* It is because {@link module:engine/model/operation/attributeoperation~AttributeOperation}
* requires a range to change and root element can't
* be a part of range because every {@link module:engine/model/position~Position} has to be inside a root.
* {@link module:engine/model/position~Position} can't be created before a root element.
*
* @extends module:engine/model/operation/operation~Operation
*/
class rootattributeoperation_RootAttributeOperation extends operation_Operation {
/**
* Creates an operation that changes, removes or adds attributes on root element.
*
* @see module:engine/model/operation/attributeoperation~AttributeOperation
* @param {module:engine/model/rootelement~RootElement} root Root element to change.
* @param {String} key Key of an attribute to change or remove.
* @param {*} oldValue Old value of the attribute with given key or `null` if adding a new attribute.
* @param {*} newValue New value to set for the attribute. If `null`, then the operation just removes the attribute.
* @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which the operation can be applied.
*/
constructor( root, key, oldValue, newValue, baseVersion ) {
super( baseVersion );
/**
* Root element to change.
*
* @readonly
* @member {module:engine/model/rootelement~RootElement}
*/
this.root = root;
/**
* Key of an attribute to change or remove.
*
* @readonly
* @member {String}
*/
this.key = key;
/**
* Old value of the attribute with given key or `null` if adding a new attribute.
*
* @readonly
* @member {*}
*/
this.oldValue = oldValue;
/**
* New value to set for the attribute. If `null`, then the operation just removes the attribute.
*
* @readonly
* @member {*}
*/
this.newValue = newValue;
}
get type() {
if ( this.oldValue === null ) {
return 'addRootAttribute';
} else if ( this.newValue === null ) {
return 'removeRootAttribute';
} else {
return 'changeRootAttribute';
}
}
/**
* Creates and returns an operation that has the same parameters as this operation.
*
* @returns {module:engine/model/operation/rootattributeoperation~RootAttributeOperation} Clone of this operation.
*/
clone() {
return new rootattributeoperation_RootAttributeOperation( this.root, this.key, this.oldValue, this.newValue, this.baseVersion );
}
/**
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
*
* @returns {module:engine/model/operation/rootattributeoperation~RootAttributeOperation}
*/
getReversed() {
return new rootattributeoperation_RootAttributeOperation( this.root, this.key, this.newValue, this.oldValue, this.baseVersion + 1 );
}
_execute() {
if ( this.oldValue !== null && this.root.getAttribute( this.key ) !== this.oldValue ) {
/**
* The attribute which should be removed does not exists for the given node.
*
* @error rootattribute-operation-wrong-old-value
* @param {module:engine/model/rootelement~RootElement} root
* @param {String} key
* @param {*} value
*/
throw new CKEditorError(
'rootattribute-operation-wrong-old-value: Changed node has different attribute value than operation\'s ' +
'old attribute value.',
{ root: this.root, key: this.key }
);
}
if ( this.oldValue === null && this.newValue !== null && this.root.hasAttribute( this.key ) ) {
/**
* The attribute with given key already exists for the given node.
*
* @error rootattribute-operation-attribute-exists
* @param {module:engine/model/rootelement~RootElement} root
* @param {String} key
*/
throw new CKEditorError(
'rootattribute-operation-attribute-exists: The attribute with given key already exists.',
{ root: this.root, key: this.key }
);
}
if ( this.newValue !== null ) {
this.root.setAttribute( this.key, this.newValue );
} else {
this.root.removeAttribute( this.key );
}
return { root: this.root, key: this.key, oldValue: this.oldValue, newValue: this.newValue };
}
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.operation.RootAttributeOperation';
}
/**
* Creates RootAttributeOperation object from deserilized object, i.e. from parsed JSON string.
*
* @param {Object} json Deserialized JSON object.
* @param {module:engine/model/document~Document} document Document on which this operation will be applied.
* @returns {module:engine/model/operation/rootattributeoperation~RootAttributeOperation}
*/
static fromJSON( json, document ) {
if ( !document.hasRoot( json.root ) ) {
/**
* Cannot create RootAttributeOperation for document. Root with specified name does not exist.
*
* @error rootattributeoperation-fromjson-no-root
* @param {String} rootName
*/
throw new CKEditorError(
'rootattribute-operation-fromjson-no-root: Cannot create RootAttributeOperation. Root with specified name does not exist.',
{ rootName: json }
);
}
return new rootattributeoperation_RootAttributeOperation( document.getRoot( json.root ), json.key, json.oldValue, json.newValue, json.baseVersion );
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operationfactory.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/operation/operationfactory
*/
const operationfactory_operations = {};
operationfactory_operations[ attributeoperation_AttributeOperation.className ] = attributeoperation_AttributeOperation;
operationfactory_operations[ insertoperation_InsertOperation.className ] = insertoperation_InsertOperation;
operationfactory_operations[ markeroperation_MarkerOperation.className ] = markeroperation_MarkerOperation;
operationfactory_operations[ moveoperation_MoveOperation.className ] = moveoperation_MoveOperation;
operationfactory_operations[ NoOperation.className ] = NoOperation;
operationfactory_operations[ operation_Operation.className ] = operation_Operation;
operationfactory_operations[ reinsertoperation_ReinsertOperation.className ] = reinsertoperation_ReinsertOperation;
operationfactory_operations[ removeoperation_RemoveOperation.className ] = removeoperation_RemoveOperation;
operationfactory_operations[ renameoperation_RenameOperation.className ] = renameoperation_RenameOperation;
operationfactory_operations[ rootattributeoperation_RootAttributeOperation.className ] = rootattributeoperation_RootAttributeOperation;
/**
* A factory class for creating operations.
*
* @abstract
*/
class OperationFactory {
/**
* Creates concrete `Operation` object from deserilized object, i.e. from parsed JSON string.
*
* @param {Object} json Deserialized JSON object.
* @param {module:engine/model/document~Document} document Document on which this operation will be applied.
* @returns {module:engine/model/operation/operation~Operation}
*/
static fromJSON( json, document ) {
return operationfactory_operations[ json.__className ].fromJSON( json, document );
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/deltafactory.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/delta/deltafactory
*/
const deserializers = new Map();
/**
* A factory class for creating operations.
*
* Delta is a single, from the user action point of view, change in the editable document, like insert, split or
* rename element. Delta is composed of operations, which are unit changes needed to be done to execute user action.
*
* Multiple deltas are grouped into a single {@link module:engine/model/batch~Batch}.
*/
class deltafactory_DeltaFactory {
/**
* Creates InsertDelta from deserialized object, i.e. from parsed JSON string.
*
* @param {Object} json
* @param {module:engine/model/document~Document} doc Document on which this delta will be applied.
* @returns {module:engine/model/delta/insertdelta~InsertDelta}
*/
static fromJSON( json, doc ) {
if ( !deserializers.has( json.__className ) ) {
/**
* This delta has no defined deserializer.
*
* @error delta-fromjson-no-deserializer
* @param {String} name
*/
throw new CKEditorError(
'delta-fromjson-no-deserializer: This delta has no defined deserializer',
{ name: json.__className }
);
}
const Delta = deserializers.get( json.__className );
const delta = new Delta();
for ( const operation of json.operations ) {
delta.addOperation( OperationFactory.fromJSON( operation, doc ) );
}
// Rewrite all other properties.
for ( const prop in json ) {
if ( prop != '__className' && delta[ prop ] === undefined ) {
delta[ prop ] = json[ prop ];
}
}
return delta;
}
/**
* Registers a class for delta factory.
*
* @param {Function} Delta A delta class to register.
*/
static register( Delta ) {
deserializers.set( Delta.className, Delta );
}
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/delta.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/delta/delta
*/
/**
* Base class for all deltas.
*
* Delta is a single, from the user action point of view, change in the editable document, like insert, split or
* rename element. Delta is composed of operations, which are unit changes needed to be done to execute user action.
*
* Multiple deltas are grouped into a single {@link module:engine/model/batch~Batch}.
*/
class delta_Delta {
/**
* Creates a delta instance.
*/
constructor() {
/**
* {@link module:engine/model/batch~Batch} which delta is a part of. This property is null by default and set by the
* {@link module:engine/model/batch~Batch#addDelta} method.
*
* @readonly
* @member {module:engine/model/batch~Batch} module:engine/model/delta/delta~Delta#batch
*/
this.batch = null;
/**
* Array of operations which compose delta.
*
* @readonly
* @member {module:engine/model/operation/operation~Operation[]} module:engine/model/delta/delta~Delta#operations
*/
this.operations = [];
}
/**
* Returns delta base version which is equal to the base version of the first operation in delta. If there
* are no operations in delta, returns `null`.
*
* @see module:engine/model/document~Document
* @type {Number|null}
*/
get baseVersion() {
if ( this.operations.length > 0 ) {
return this.operations[ 0 ].baseVersion;
}
return null;
}
/**
* @param {Number} baseVersion
*/
set baseVersion( baseVersion ) {
for ( const operation of this.operations ) {
operation.baseVersion = baseVersion++;
}
}
/**
* A class that will be used when creating reversed delta.
*
* @private
* @type {Function}
*/
get _reverseDeltaClass() {
return delta_Delta;
}
/**
* Delta type.
*
* @readonly
* @member {String} #type
*/
/**
* Add operation to the delta.
*
* @param {module:engine/model/operation/operation~Operation} operation Operation instance.
*/
addOperation( operation ) {
operation.delta = this;
this.operations.push( operation );
return operation;
}
/**
* Creates and returns a delta that has the same parameters as this delta.
*
* @returns {module:engine/model/delta/delta~Delta} Clone of this delta.
*/
clone() {
const delta = new this.constructor();
for ( const op of this.operations ) {
delta.addOperation( op.clone() );
}
return delta;
}
/**
* Creates and returns a reverse delta. Reverse delta when executed right after the original delta will bring back
* tree model state to the point before the original delta execution. In other words, it reverses changes done
* by the original delta.
*
* Keep in mind that tree model state may change since executing the original delta, so reverse delta may be "outdated".
* In that case you will need to {@link module:engine/model/delta/transform~transform} it by all deltas that were executed after
* the original delta.
*
* @returns {module:engine/model/delta/delta~Delta} Reversed delta.
*/
getReversed() {
const delta = new this._reverseDeltaClass();
for ( const op of this.operations ) {
delta.addOperation( op.getReversed() );
}
delta.operations.reverse();
for ( let i = 0; i < delta.operations.length; i++ ) {
delta.operations[ i ].baseVersion = this.operations[ this.operations.length - 1 ].baseVersion + i + 1;
}
return delta;
}
/**
* Custom toJSON method to make deltas serializable.
*
* @returns {Object} Clone of this delta with added class name.
*/
toJSON() {
const json = lodash_clone( this );
json.__className = this.constructor.className;
// Remove parent batch to avoid circular dependencies.
delete json.batch;
return json;
}
/**
* Delta class name. Used by {@link #toJSON} method for serialization and
* {@link module:engine/model/delta/deltafactory~DeltaFactory.fromJSON} during deserialization.
*
* @type {String}
* @readonly
*/
static get className() {
return 'engine.model.delta.Delta';
}
}
deltafactory_DeltaFactory.register( delta_Delta );
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/batch.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/batch
*/
/**
* `Batch` instance groups document changes ({@link module:engine/model/delta/delta~Delta deltas}). All deltas grouped in a single `Batch`
* can be reverted together, so you can think about `Batch` as of a single undo step. If you want to extend given undo step you
* can call another method on the same `Batch` object. If you want to create a separate undo step you can create a new `Batch`.
*
* For example to create two separate undo steps you can call:
*
* doc.batch().insert( firstPosition, 'foo' );
* doc.batch().insert( secondPosition, 'bar' );
*
* To create a single undo step:
*
* const batch = doc.batch();
* batch.insert( firstPosition, 'foo' );
* batch.insert( secondPosition, 'bar' );
*
* Note that all document modification methods (insert, remove, split, etc.) are chainable so you can shorten code to:
*
* doc.batch().insert( firstPosition, 'foo' ).insert( secondPosition, 'bar' );
*/
class Batch {
/**
* Creates `Batch` instance. Not recommended to use directly, use {@link module:engine/model/document~Document#batch} instead.
*
* @param {module:engine/model/document~Document} document Document which this Batch changes.
* @param {'transparent'|'default'} [type='default'] Type of the batch.
*/
constructor( document, type = 'default' ) {
/**
* Document which this batch changes.
*
* @readonly
* @member {module:engine/model/document~Document} module:engine/model/batch~Batch#document
*/
this.document = document;
/**
* Array of deltas which compose this batch.
*
* @readonly
* @member {Array.<module:engine/model/delta/delta~Delta>} module:engine/model/batch~Batch#deltas
*/
this.deltas = [];
/**
* Type of the batch.
*
* Can be one of the following values:
* * `'default'` - all "normal" batches, most commonly used type.
* * `'transparent'` - batch that should be ignored by other features, i.e. initial batch or collaborative editing changes.
*
* @readonly
* @member {'transparent'|'default'} module:engine/model/batch~Batch#type
*/
this.type = type;
}
/**
* Returns this batch base version, which is equal to the base version of first delta in the batch.
* If there are no deltas in the batch, it returns `null`.
*
* @readonly
* @type {Number|null}
*/
get baseVersion() {
return this.deltas.length > 0 ? this.deltas[ 0 ].baseVersion : null;
}
/**
* Adds delta to the batch instance. All modification methods (insert, remove, split, etc.) use this method
* to add created deltas.
*
* @param {module:engine/model/delta/delta~Delta} delta Delta to add.
* @return {module:engine/model/delta/delta~Delta} Added delta.
*/
addDelta( delta ) {
delta.batch = this;
this.deltas.push( delta );
return delta;
}
/**
* Gets an iterable collection of operations.
*
* @returns {Iterable.<module:engine/model/operation/operation~Operation>}
*/
* getOperations() {
for ( const delta of this.deltas ) {
yield* delta.operations;
}
}
}
/**
* Function to register batch methods. To make code scalable `Batch` do not have modification
* methods built in. They can be registered using this method.
*
* This method checks if there is no naming collision and throws `batch-register-taken` if the method name
* is already taken.
*
* Besides that no magic happens here, the method is added to the `Batch` class prototype.
*
* For example:
*
* Batch.register( 'insert', function( position, nodes ) {
* // You can use a class inheriting from `Delta` if that class should handle OT in a special way.
* const delta = new Delta();
*
* // Add delta to the Batch instance. It is important to add a delta to the batch before applying any operation.
* this.addDelta( delta );
*
* // Create operations which should be components of this delta.
* const operation = new InsertOperation( position, nodes, this.document.version );
*
* // Add operation to the delta. It is important to add operation before applying it.
* delta.addOperation( operation );
*
* // Remember to apply every operation, no magic, you need to do it manually.
* this.document.applyOperation( operation );
*
* // Make this method chainable.
* return this;
* } );
*
* @method module:engine/model/batch~Batch.register
* @param {String} name Method name.
* @param {Function} creator Method body.
*/
function register( name, creator ) {
if ( Batch.prototype[ name ] ) {
/**
* This batch method name is already taken.
*
* @error batch-register-taken
* @param {String} name
*/
throw new CKEditorError(
'model-batch-register-taken: This batch method name is already taken.',
{ name } );
}
Batch.prototype[ name ] = creator;
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/attributedelta.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/delta/attributedelta
*/
/**
* To provide specific OT behavior and better collisions solving, methods to change attributes
* ({@link module:engine/model/batch~Batch#setAttribute} and {@link module:engine/model/batch~Batch#removeAttribute})
* use `AttributeDelta` class which inherits from the `Delta` class and may overwrite some methods.
*
* @extends module:engine/model/delta/delta~Delta
*/
class attributedelta_AttributeDelta extends delta_Delta {
/**
* @inheritDoc
*/
get type() {
return 'attribute';
}
/**
* The attribute key that is changed by the delta or `null` if the delta has no operations.
*
* @readonly
* @type {String|null}
*/
get key() {
return this.operations[ 0 ] ? this.operations[ 0 ].key : null;
}
/**
* The attribute value that is set by the delta or `null` if the delta has no operations.
*
* @readonly
* @type {*|null}
*/
get value() {
return this.operations[ 0 ] ? this.operations[ 0 ].newValue : null;
}
/**
* The range on which delta operates or `null` if the delta has no operations.
*
* @readonly
* @type {module:engine/model/range~Range|null}
*/
get range() {
// Check if it is cached.
if ( this._range ) {
return this._range;
}
let start = null;
let end = null;
for ( const operation of this.operations ) {
if ( operation instanceof NoOperation ) {
continue;
}
if ( start === null || start.isAfter( operation.range.start ) ) {
start = operation.range.start;
}
if ( end === null || end.isBefore( operation.range.end ) ) {
end = operation.range.end;
}
}
if ( start && end ) {
this._range = new range_Range( start, end );
return this._range;
}
return null;
}
get _reverseDeltaClass() {
return attributedelta_AttributeDelta;
}
/**
* @inheritDoc
*/
toJSON() {
const json = super.toJSON();
delete json._range;
return json;
}
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.delta.AttributeDelta';
}
}
/**
* To provide specific OT behavior and better collisions solving, methods to change attributes
* ({@link module:engine/model/batch~Batch#setAttribute} and {@link module:engine/model/batch~Batch#removeAttribute})
* use `RootAttributeDelta` class which inherits from the `Delta` class and may
* overwrite some methods.
*
* @extends module:engine/model/delta/delta~Delta
*/
class RootAttributeDelta extends delta_Delta {
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.delta.RootAttributeDelta';
}
}
/**
* Sets value of the attribute with given key on a {@link module:engine/model/item~Item model item}
* or on a {@link module:engine/model/range~Range range}.
*
* @chainable
* @method module:engine/model/batch~Batch#setAttribute
* @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
* Model item or range on which the attribute will be set.
* @param {String} key Attribute key.
* @param {*} value Attribute new value.
*/
register( 'setAttribute', function( itemOrRange, key, value ) {
attributedelta_attribute( this, key, value, itemOrRange );
return this;
} );
/**
* Removes an attribute with given key from a {@link module:engine/model/item~Item model item}
* or from a {@link module:engine/model/range~Range range}.
*
* @chainable
* @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
* Model item or range from which the attribute will be removed.
* @method module:engine/model/batch~Batch#removeAttribute
* @param {String} key Attribute key.
*/
register( 'removeAttribute', function( itemOrRange, key ) {
attributedelta_attribute( this, key, null, itemOrRange );
return this;
} );
function attributedelta_attribute( batch, key, value, itemOrRange ) {
if ( itemOrRange instanceof range_Range ) {
changeRange( batch, batch.document, key, value, itemOrRange );
} else {
changeItem( batch, batch.document, key, value, itemOrRange );
}
}
function changeItem( batch, doc, key, value, item ) {
const previousValue = item.getAttribute( key );
let range, operation;
const delta = item.is( 'rootElement' ) ? new RootAttributeDelta() : new attributedelta_AttributeDelta();
if ( previousValue != value ) {
batch.addDelta( delta );
if ( item.is( 'rootElement' ) ) {
// If we change attributes of root element, we have to use `RootAttributeOperation`.
operation = new rootattributeoperation_RootAttributeOperation( item, key, previousValue, value, doc.version );
} else {
if ( item.is( 'element' ) ) {
// If we change the attribute of the element, we do not want to change attributes of its children, so
// the end of the range cannot be after the closing tag, it should be inside that element, before any of
// it's children, so the range will contain only the opening tag.
range = new range_Range( position_Position.createBefore( item ), position_Position.createFromParentAndOffset( item, 0 ) );
} else {
// If `item` is text proxy, we create a range from the beginning to the end of that text proxy, to change
// all characters represented by it.
range = new range_Range( position_Position.createBefore( item ), position_Position.createAfter( item ) );
}
operation = new attributeoperation_AttributeOperation( range, key, previousValue, value, doc.version );
}
delta.addOperation( operation );
doc.applyOperation( operation );
}
}
// Because attribute operation needs to have the same attribute value on the whole range, this function splits the range
// into smaller parts.
function changeRange( batch, doc, attributeKey, attributeValue, range ) {
const delta = new attributedelta_AttributeDelta();
// Position of the last split, the beginning of the new range.
let lastSplitPosition = range.start;
// Currently position in the scanning range. Because we need value after the position, it is not a current
// position of the iterator but the previous one (we need to iterate one more time to get the value after).
let position,
// Value before the currently position.
attributeValueBefore,
// Value after the currently position.
attributeValueAfter;
for ( const value of range ) {
attributeValueAfter = value.item.getAttribute( attributeKey );
// At the first run of the iterator the position in undefined. We also do not have a attributeValueBefore, but
// because attributeValueAfter may be null, attributeValueBefore may be equal attributeValueAfter ( undefined == null ).
if ( position && attributeValueBefore != attributeValueAfter ) {
// if attributeValueBefore == attributeValue there is nothing to change, so we add operation only if these values are different.
if ( attributeValueBefore != attributeValue ) {
addOperation();
}
lastSplitPosition = position;
}
position = value.nextPosition;
attributeValueBefore = attributeValueAfter;
}
// Because position in the loop is not the iterator position (see let position comment), the last position in
// the while loop will be last but one position in the range. We need to check the last position manually.
if ( position instanceof position_Position && position != lastSplitPosition && attributeValueBefore != attributeValue ) {
addOperation();
}
function addOperation() {
// Add delta to the batch only if there is at least operation in the delta. Add delta only once.
if ( delta.operations.length === 0 ) {
batch.addDelta( delta );
}
const range = new range_Range( lastSplitPosition, position );
const operation = new attributeoperation_AttributeOperation( range, attributeKey, attributeValueBefore, attributeValue, doc.version );
delta.addOperation( operation );
doc.applyOperation( operation );
}
}
deltafactory_DeltaFactory.register( attributedelta_AttributeDelta );
deltafactory_DeltaFactory.register( RootAttributeDelta );
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/movedelta.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/delta/movedelta
*/
/**
* To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#move} method
* uses the `MoveDelta` class which inherits from the `Delta` class and may overwrite some methods.
*
* @extends module:engine/model/delta/delta~Delta
*/
class MoveDelta extends delta_Delta {
/**
* @inheritDoc
*/
get type() {
return 'move';
}
/**
* Offset size of moved range or `null` if there are no operations in the delta.
*
* @type {Number|null}
*/
get howMany() {
return this._moveOperation ? this._moveOperation.howMany : null;
}
/**
* {@link module:engine/model/delta/movedelta~MoveDelta#_moveOperation Move operation}
* {@link module:engine/model/operation/moveoperation~MoveOperation#sourcePosition source position} or `null` if there are
* no operations in the delta.
*
* @type {module:engine/model/position~Position|null}
*/
get sourcePosition() {
return this._moveOperation ? this._moveOperation.sourcePosition : null;
}
/**
* {@link module:engine/model/delta/movedelta~MoveDelta#_moveOperation Move operation}
* {@link module:engine/model/operation/moveoperation~MoveOperation#targetPosition target position} or `null` if there are
* no operations in the delta.
*
* @type {module:engine/model/position~Position|null}
*/
get targetPosition() {
return this._moveOperation ? this._moveOperation.targetPosition : null;
}
/**
* {@link module:engine/model/delta/movedelta~MoveDelta#_moveOperation Move operation} that is saved in this delta or `null`
* if there are no operations in the delta.
*
* @protected
* @type {module:engine/model/operation/moveoperation~MoveOperation|null}
*/
get _moveOperation() {
return this.operations[ 0 ] || null;
}
/**
* @inheritDoc
*/
get _reverseDeltaClass() {
return MoveDelta;
}
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.delta.MoveDelta';
}
}
function addMoveOperation( batch, delta, sourcePosition, howMany, targetPosition ) {
const operation = new moveoperation_MoveOperation( sourcePosition, howMany, targetPosition, batch.document.version );
delta.addOperation( operation );
batch.document.applyOperation( operation );
}
/**
* Moves given {@link module:engine/model/item~Item model item} or given range to target position.
*
* @chainable
* @method module:engine/model/batch~Batch#move
* @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range of nodes to move.
* @param {module:engine/model/position~Position} targetPosition Position where moved nodes will be inserted.
*/
register( 'move', function( itemOrRange, targetPosition ) {
const delta = new MoveDelta();
this.addDelta( delta );
if ( itemOrRange instanceof range_Range ) {
if ( !itemOrRange.isFlat ) {
/**
* Range to move is not flat.
*
* @error batch-move-range-not-flat
*/
throw new CKEditorError( 'batch-move-range-not-flat: Range to move is not flat.' );
}
addMoveOperation( this, delta, itemOrRange.start, itemOrRange.end.offset - itemOrRange.start.offset, targetPosition );
} else {
addMoveOperation( this, delta, position_Position.createBefore( itemOrRange ), 1, targetPosition );
}
return this;
} );
deltafactory_DeltaFactory.register( MoveDelta );
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/removedelta.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/delta/removedelta
*/
/**
* To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#remove} method
* uses the `RemoveDelta` class which inherits from the `Delta` class and may overwrite some methods.
*
* @extends module:engine/model/delta/delta~Delta
*/
class RemoveDelta extends MoveDelta {
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.delta.RemoveDelta';
}
}
function addRemoveDelta( batch, position, howMany ) {
const delta = new RemoveDelta();
batch.addDelta( delta );
const graveyard = batch.document.graveyard;
const gyPosition = new position_Position( graveyard, [ 0 ] );
const operation = new removeoperation_RemoveOperation( position, howMany, gyPosition, batch.document.version );
delta.addOperation( operation );
batch.document.applyOperation( operation );
}
/**
* Removes given {@link module:engine/model/item~Item model item} or given range.
*
* @chainable
* @method module:engine/model/batch~Batch#remove
* @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range to remove.
*/
register( 'remove', function( itemOrRange ) {
if ( itemOrRange instanceof range_Range ) {
// The array is reversed, so the ranges to remove are in correct order and do not have to be updated.
const ranges = itemOrRange.getMinimalFlatRanges().reverse();
for ( const flat of ranges ) {
addRemoveDelta( this, flat.start, flat.end.offset - flat.start.offset );
}
} else {
addRemoveDelta( this, position_Position.createBefore( itemOrRange ), 1 );
}
return this;
} );
deltafactory_DeltaFactory.register( RemoveDelta );
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/insertdelta.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/delta/insertdelta
*/
/**
* To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#insert Batch#insert} method
* uses the `InsertDelta` class which inherits from the `Delta` class and may overwrite some methods.
*
* @extends module:engine/model/delta/delta~Delta
*/
class insertdelta_InsertDelta extends delta_Delta {
/**
* @inheritDoc
*/
get type() {
return 'insert';
}
/**
* Position where the delta inserts nodes or `null` if there are no operations in the delta.
*
* @readonly
* @type {module:engine/model/position~Position|null}
*/
get position() {
return this._insertOperation ? this._insertOperation.position : null;
}
/**
* Node list containing all the nodes inserted by the delta or `null` if there are no operations in the delta.
*
* @readonly
* @type {module:engine/model/nodelist~NodeList|null}
*/
get nodes() {
return this._insertOperation ? this._insertOperation.nodes : null;
}
/**
* Insert operation that is saved in this delta or `null` if there are no operations in the delta.
*
* @readonly
* @protected
* @type {module:engine/model/operation/insertoperation~InsertOperation|null}
*/
get _insertOperation() {
return this.operations[ 0 ] || null;
}
/**
* @inheritDoc
*/
get _reverseDeltaClass() {
return RemoveDelta;
}
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.delta.InsertDelta';
}
}
/**
* Inserts a node or nodes at the given position.
*
* When inserted element is a {@link module:engine/model/documentfragment~DocumentFragment} and has markers its markers will
* be set to {@link module:engine/model/document~Document#markers}.
*
* @chainable
* @method module:engine/model/batch~Batch#insert
* @param {module:engine/model/position~Position} position Position of insertion.
* @param {module:engine/model/node~NodeSet} nodes The list of nodes to be inserted.
*/
register( 'insert', function( position, nodes ) {
const normalizedNodes = normalizeNodes( nodes );
// If nothing is inserted do not create delta and operation.
if ( normalizedNodes.length === 0 ) {
return this;
}
const delta = new insertdelta_InsertDelta();
const insert = new insertoperation_InsertOperation( position, normalizedNodes, this.document.version );
this.addDelta( delta );
delta.addOperation( insert );
this.document.applyOperation( insert );
// When element is a DocumentFragment we need to move its markers to Document#markers.
if ( nodes instanceof documentfragment_DocumentFragment ) {
for ( const [ markerName, markerRange ] of nodes.markers ) {
// We need to migrate marker range from DocumentFragment to Document.
const rangeRootPosition = position_Position.createAt( markerRange.root );
const range = new range_Range(
markerRange.start._getCombined( rangeRootPosition, position ),
markerRange.end._getCombined( rangeRootPosition, position )
);
this.setMarker( markerName, range );
}
}
return this;
} );
deltafactory_DeltaFactory.register( insertdelta_InsertDelta );
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/splitdelta.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/delta/splitdelta
*/
/**
* To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#split} method
* uses `SplitDelta` class which inherits from the `Delta` class and may overwrite some methods.
*
* @extends module:engine/model/delta/delta~Delta
*/
class splitdelta_SplitDelta extends delta_Delta {
/**
* @inheritDoc
*/
get type() {
return 'split';
}
/**
* Position of split or `null` if there are no operations in the delta.
*
* @type {module:engine/model/position~Position|null}
*/
get position() {
return this._moveOperation ? this._moveOperation.sourcePosition : null;
}
/**
* Operation in the delta that adds to model an element into which split nodes will be moved, or `null` if
* there are no operations in the delta.
*
* Most commonly this will be {@link module:engine/model/operation/insertoperation~InsertOperation an insert operation},
* as `SplitDelta` has to create a new node. If `SplitDelta` was created through
* {@link module:engine/model/delta/delta~Delta#getReversed reversing}
* a {@link module:engine/model/delta/mergedelta~MergeDelta merge delta},
* this will be a {@link module:engine/model/operation/reinsertoperation~ReinsertOperation reinsert operation},
* as we will want to re-insert the exact element that was removed by that merge delta.
*
* @protected
* @type {module:engine/model/operation/insertoperation~InsertOperation|
* module:engine/model/operation/reinsertoperation~ReinsertOperation|null}
*/
get _cloneOperation() {
return this.operations[ 0 ] || null;
}
/**
* Operation in the delta that moves model items, that are after split position, to their new parent or `null`
* if there are no operations in the delta.
*
* @protected
* @type {module:engine/model/operation/moveoperation~MoveOperation|null}
*/
get _moveOperation() {
return this.operations[ 1 ] && this.operations[ 1 ] instanceof moveoperation_MoveOperation ? this.operations[ 1 ] : null;
}
/**
* @inheritDoc
*/
get _reverseDeltaClass() {
return mergedelta_MergeDelta;
}
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.delta.SplitDelta';
}
}
/**
* Splits an element at the given position.
*
* The element cannot be a root element, as root element cannot be split. The `batch-split-root` error will be thrown if
* you try to split the root element.
*
* @chainable
* @method module:engine/model/batch~Batch#split
* @param {module:engine/model/position~Position} position Position of split.
*/
register( 'split', function( position ) {
const delta = new splitdelta_SplitDelta();
this.addDelta( delta );
const splitElement = position.parent;
if ( !splitElement.parent ) {
/**
* Root element can not be split.
*
* @error batch-split-root
*/
throw new CKEditorError( 'batch-split-root: Root element can not be split.' );
}
const copy = new element_Element( splitElement.name, splitElement.getAttributes() );
const insert = new insertoperation_InsertOperation(
position_Position.createAfter( splitElement ),
copy,
this.document.version
);
delta.addOperation( insert );
this.document.applyOperation( insert );
const move = new moveoperation_MoveOperation(
position,
splitElement.maxOffset - position.offset,
position_Position.createFromParentAndOffset( copy, 0 ),
this.document.version
);
move.isSticky = true;
delta.addOperation( move );
this.document.applyOperation( move );
return this;
} );
deltafactory_DeltaFactory.register( splitdelta_SplitDelta );
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/mergedelta.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/delta/mergedelta
*/
/**
* To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#merge} method
* uses the `MergeDelta` class which inherits from the `Delta` class and may overwrite some methods.
*
* @extends module:engine/model/delta/delta~Delta
*/
class mergedelta_MergeDelta extends delta_Delta {
/**
* @inheritDoc
*/
get type() {
return 'merge';
}
/**
* Position between to merged nodes or `null` if the delta has no operations.
*
* @readonly
* @type {module:engine/model/position~Position|null}
*/
get position() {
return this._removeOperation ? this._removeOperation.sourcePosition : null;
}
/**
* Operation in this delta that removes the node after merge position (which will be empty at that point) or
* `null` if the delta has no operations. Note, that after {@link module:engine/model/delta/transform~transform transformation}
* this might be an instance of {@link module:engine/model/operation/moveoperation~MoveOperation} instead of
* {@link module:engine/model/operation/removeoperation~RemoveOperation}.
*
* @readonly
* @protected
* @type {module:engine/model/operation/moveoperation~MoveOperation|null}
*/
get _removeOperation() {
return this.operations[ 1 ] || null;
}
/**
* @inheritDoc
*/
get _reverseDeltaClass() {
return splitdelta_SplitDelta;
}
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.delta.MergeDelta';
}
}
/**
* Merges two siblings at the given position.
*
* Node before and after the position have to be an element. Otherwise `batch-merge-no-element-before` or
* `batch-merge-no-element-after` error will be thrown.
*
* @chainable
* @method module:engine/model/batch~Batch#merge
* @param {module:engine/model/position~Position} position Position of merge.
*/
register( 'merge', function( position ) {
const delta = new mergedelta_MergeDelta();
this.addDelta( delta );
const nodeBefore = position.nodeBefore;
const nodeAfter = position.nodeAfter;
if ( !( nodeBefore instanceof element_Element ) ) {
/**
* Node before merge position must be an element.
*
* @error batch-merge-no-element-before
*/
throw new CKEditorError( 'batch-merge-no-element-before: Node before merge position must be an element.' );
}
if ( !( nodeAfter instanceof element_Element ) ) {
/**
* Node after merge position must be an element.
*
* @error batch-merge-no-element-after
*/
throw new CKEditorError( 'batch-merge-no-element-after: Node after merge position must be an element.' );
}
const positionAfter = position_Position.createFromParentAndOffset( nodeAfter, 0 );
const positionBefore = position_Position.createFromParentAndOffset( nodeBefore, nodeBefore.maxOffset );
const move = new moveoperation_MoveOperation(
positionAfter,
nodeAfter.maxOffset,
positionBefore,
this.document.version
);
move.isSticky = true;
delta.addOperation( move );
this.document.applyOperation( move );
const graveyard = this.document.graveyard;
const gyPosition = new position_Position( graveyard, [ 0 ] );
const remove = new removeoperation_RemoveOperation( position, 1, gyPosition, this.document.version );
delta.addOperation( remove );
this.document.applyOperation( remove );
return this;
} );
deltafactory_DeltaFactory.register( mergedelta_MergeDelta );
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/renamedelta.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/delta/renamedelta
*/
/**
* To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#rename Batch#rename} method
* uses the `RenameDelta` class which inherits from the `Delta` class and may overwrite some methods.
*
* @extends module:engine/model/delta/delta~Delta
*/
class RenameDelta extends delta_Delta {
/**
* @inheritDoc
*/
get type() {
return 'rename';
}
/**
* @inheritDoc
*/
get _reverseDeltaClass() {
return RenameDelta;
}
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.delta.RenameDelta';
}
}
function renamedelta_apply( batch, delta, operation ) {
delta.addOperation( operation );
batch.document.applyOperation( operation );
}
/**
* Renames given element.
*
* @chainable
* @method module:engine/model/batch~Batch#rename
* @param {module:engine/model/element~Element} element The element to rename.
* @param {String} newName New element name.
*/
register( 'rename', function( element, newName ) {
if ( !( element instanceof element_Element ) ) {
/**
* Trying to rename an object which is not an instance of Element.
*
* @error batch-rename-not-element-instance
*/
throw new CKEditorError( 'batch-rename-not-element-instance: Trying to rename an object which is not an instance of Element.' );
}
const delta = new RenameDelta();
this.addDelta( delta );
const renameOperation = new renameoperation_RenameOperation( position_Position.createBefore( element ), element.name, newName, this.document.version );
renamedelta_apply( this, delta, renameOperation );
return this;
} );
deltafactory_DeltaFactory.register( RenameDelta );
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/wrapdelta.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/delta/wrapdelta
*/
/**
* To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#merge} method
* uses the `WrapDelta` class which inherits from the `Delta` class and may overwrite some methods.
*
* @extends module:engine/model/delta/delta~Delta
*/
class wrapdelta_WrapDelta extends delta_Delta {
/**
* @inheritDoc
*/
get type() {
return 'wrap';
}
/**
* Range to wrap or `null` if there are no operations in the delta.
*
* @type {module:engine/model/range~Range|null}
*/
get range() {
const moveOp = this._moveOperation;
return moveOp ? range_Range.createFromPositionAndShift( moveOp.sourcePosition, moveOp.howMany ) : null;
}
/**
* Offset size of range to wrap by the delta or `null` if there are no operations in delta.
*
* @type {Number}
*/
get howMany() {
const range = this.range;
return range ? range.end.offset - range.start.offset : 0;
}
/* eslint-disable max-len */
/**
* Operation that inserts wrapping element or `null` if there are no operations in the delta.
*
* @protected
* @type {module:engine/model/operation/insertoperation~InsertOperation|module:engine/model/operation/reinsertoperation~ReinsertOperation}
*/
/* eslint-enable max-len */
get _insertOperation() {
return this.operations[ 0 ] || null;
}
/**
* Operation that moves wrapped nodes to their new parent or `null` if there are no operations in the delta.
*
* @protected
* @type {module:engine/model/operation/moveoperation~MoveOperation|null}
*/
get _moveOperation() {
return this.operations[ 1 ] || null;
}
/**
* @inheritDoc
*/
get _reverseDeltaClass() {
return unwrapdelta_UnwrapDelta;
}
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.delta.WrapDelta';
}
}
/**
* Wraps given range with given element or with a new element with specified name, if string has been passed.
* **Note:** range to wrap should be a "flat range" (see {@link module:engine/model/range~Range#isFlat}). If not, error will be thrown.
*
* @chainable
* @method module:engine/model/batch~Batch#wrap
* @param {module:engine/model/range~Range} range Range to wrap.
* @param {module:engine/model/element~Element|String} elementOrString Element or name of element to wrap the range with.
*/
register( 'wrap', function( range, elementOrString ) {
if ( !range.isFlat ) {
/**
* Range to wrap is not flat.
*
* @error batch-wrap-range-not-flat
*/
throw new CKEditorError( 'batch-wrap-range-not-flat: Range to wrap is not flat.' );
}
const element = elementOrString instanceof element_Element ? elementOrString : new element_Element( elementOrString );
if ( element.childCount > 0 ) {
/**
* Element to wrap with is not empty.
*
* @error batch-wrap-element-not-empty
*/
throw new CKEditorError( 'batch-wrap-element-not-empty: Element to wrap with is not empty.' );
}
if ( element.parent !== null ) {
/**
* Element to wrap with is already attached to a tree model.
*
* @error batch-wrap-element-attached
*/
throw new CKEditorError( 'batch-wrap-element-attached: Element to wrap with is already attached to tree model.' );
}
const delta = new wrapdelta_WrapDelta();
this.addDelta( delta );
const insert = new insertoperation_InsertOperation( range.end, element, this.document.version );
delta.addOperation( insert );
this.document.applyOperation( insert );
const targetPosition = position_Position.createFromParentAndOffset( element, 0 );
const move = new moveoperation_MoveOperation(
range.start,
range.end.offset - range.start.offset,
targetPosition,
this.document.version
);
delta.addOperation( move );
this.document.applyOperation( move );
return this;
} );
deltafactory_DeltaFactory.register( wrapdelta_WrapDelta );
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/unwrapdelta.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/delta/unwrapdelta
*/
/**
* To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#merge} method
* uses the `UnwrapDelta` class which inherits from the `Delta` class and may overwrite some methods.
*
* @extends module:engine/model/delta/delta~Delta
*/
class unwrapdelta_UnwrapDelta extends delta_Delta {
/**
* @inheritDoc
*/
get type() {
return 'unwrap';
}
/**
* Position before unwrapped element or `null` if there are no operations in the delta.
*
* @type {module:engine/model/position~Position|null}
*/
get position() {
return this._moveOperation ? this._moveOperation.targetPosition : null;
}
/**
* Operation in the delta that moves unwrapped nodes to their new parent or `null` if there are no operations in the delta.
*
* @protected
* @type {module:engine/model/operation/moveoperation~MoveOperation|null}
*/
get _moveOperation() {
return this.operations[ 0 ] || null;
}
/**
* @inheritDoc
*/
get _reverseDeltaClass() {
return wrapdelta_WrapDelta;
}
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.delta.UnwrapDelta';
}
}
/**
* Unwraps children of the given element – all its children are moved before it and then the element is removed.
* Throws error if you try to unwrap an element which does not have a parent.
*
* @chainable
* @method module:engine/model/batch~Batch#unwrap
* @param {module:engine/model/element~Element} position Element to unwrap.
*/
register( 'unwrap', function( element ) {
if ( element.parent === null ) {
/**
* Trying to unwrap an element which has no parent.
*
* @error batch-unwrap-element-no-parent
*/
throw new CKEditorError( 'batch-unwrap-element-no-parent: Trying to unwrap an element which has no parent.' );
}
const delta = new unwrapdelta_UnwrapDelta();
this.addDelta( delta );
const sourcePosition = position_Position.createFromParentAndOffset( element, 0 );
const move = new moveoperation_MoveOperation(
sourcePosition,
element.maxOffset,
position_Position.createBefore( element ),
this.document.version
);
move.isSticky = true;
delta.addOperation( move );
this.document.applyOperation( move );
// Computing new position because we moved some nodes before `element`.
// If we would cache `Position.createBefore( element )` we remove wrong node.
const graveyard = this.document.graveyard;
const gyPosition = new position_Position( graveyard, [ 0 ] );
const remove = new removeoperation_RemoveOperation( position_Position.createBefore( element ), 1, gyPosition, this.document.version );
delta.addOperation( remove );
this.document.applyOperation( remove );
return this;
} );
deltafactory_DeltaFactory.register( unwrapdelta_UnwrapDelta );
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/weakinsertdelta.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/delta/weakinsertdelta
*/
/**
* To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#insert} method
* uses the `WeakInsertDelta` class which inherits from the `Delta` class and may overwrite some methods.
*
* @extends module:engine/model/delta/delta~Delta
*/
class WeakInsertDelta extends insertdelta_InsertDelta {
/**
* @inheritDoc
*/
static get className() {
return 'engine.model.delta.WeakInsertDelta';
}
}
/**
* Inserts a node or nodes at given position. {@link module:engine/model/batch~Batch#weakInsert weakInsert} is commonly used for actions
* like typing or plain-text paste (without formatting). There are two differences between
* {@link module:engine/model/batch~Batch#insert insert} and {@link module:engine/model/batch~Batch#weakInsert weakInsert}:
*
* * When using `weakInsert`, inserted nodes will have same attributes as the current attributes of
* {@link module:engine/model/document~Document#selection document selection}.
* * If {@link module:engine/model/operation/insertoperation~InsertOperation insert operation} position is inside a range changed by
* {@link module:engine/model/operation/attributeoperation~AttributeOperation attribute operation},
* the attribute operation is split into two operations.
* Thanks to this, attribute change "omits" the inserted nodes. The correct behavior for `WeakInsertDelta` is that
* {@link module:engine/model/operation/attributeoperation~AttributeOperation AttributeOperation} does not "break" and also
* applies attributes for inserted nodes. This behavior has to be reflected during
* {@link module:engine/model/delta/transform~transform delta transformation}.
*
* @chainable
* @method module:engine/model/batch~Batch#weakInsert
* @param {module:engine/model/position~Position} position Position of insertion.
* @param {module:engine/model/node~NodeSet} nodes The list of nodes to be inserted.
*/
register( 'weakInsert', function( position, nodes ) {
const delta = new WeakInsertDelta();
this.addDelta( delta );
nodes = normalizeNodes( nodes );
for ( const node of nodes ) {
node.setAttributesTo( this.document.selection.getAttributes() );
}
const operation = new insertoperation_InsertOperation( position, nodes, this.document.version );
delta.addOperation( operation );
this.document.applyOperation( operation );
return this;
} );
deltafactory_DeltaFactory.register( WeakInsertDelta );
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/basic-deltas.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/delta/basic-deltas
*/
// Deltas require `register` method that require `Batch` class and is defined in batch-base.js.
// We would like to group all deltas files in one place, so we would only have to include batch.js
// which would already have all default deltas registered.
// Import default suite of deltas so a feature have to include only Batch class file.
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/transform.js
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module engine/model/operation/transform
*/
/**
* Transforms given {@link module:engine/model/operation/operation~Operation operation}
* by another {@link module:engine/model/operation/operation~Operation operation}
* and returns the result of that transformation as an array containing
* one or more {@link module:engine/model/operation/operation~Operation operations}.
*
* Operations work on specified positions, passed to them when they are created.
* Whenever {@link module:engine/model/document~Document document}
* changes, we have to reflect those modifications by updating or "transforming" operations which are not yet applied.
* When an operation is transformed, its parameters may change based on the operation by which it is transformed.
* If the transform-by operation applied any modifications to the Tree Data Model which affect positions or nodes
* connected with transformed operation, those changes will be reflected in the parameters of the returned operation(s).
*
* Whenever the {@link module:engine/model/document~Document document}
* has different {@link module:engine/model/document~Document#version}
* than the operation you want to {@link module:engine/model/document~Document#applyOperation apply}, you need to transform that
* operation by all operations which were already applied to the {@link module:engine/model/document~Document document} and have greater
* {@link module:engine/model/document~Document#version} than the operation being applied. Transform them in the same order as those
* operations which were applied. This way all modifications done to the Tree Data Model will be reflected
* in the operation parameters and the operation will "operate" on "up-to-date" version of the Tree Data Model.
* This is mostly the case with Operational Transformations but it might be needed in particular features as well.
*
* In some cases, when given operation apply changes to the same nodes as this operation, two or more operations need
* to be created as one would not be able to reflect the combination of these operations.
* This is why an array is returned instead of a single object. All returned operations have to be applied
* (or further transformed) to get an effect which was intended in pre-transformed operation.
*
* Sometimes two operations are in conflict. This happens when they modify the same node in a different way, i.e.
* set different value for the same attribute or move the node into different positions. When this happens,
* we need to decide which operation is more important. We can't assume that operation `a` or operation `b` is always
* more important. In Operational Transformations algorithms we often need to get a result of transforming
* `a` by `b` and also `b` by `a`. In both transformations the same operation has to be the important one. If we assume
* that first or the second passed operation is always more important we won't be able to solve this case.
*
* @function module:engine/model/operation/transform~transform
* @param {module:engine/model/operation/operation~Operation} a Operation that will be transformed.
* @param {module:engine/model/operation/operation~Operation} b Operation to transform by.
* @param {module:engine/model/delta/transform~transformationContext} [context] Transformation context.
* @returns {Array.<module:engine/model/operation/operation~Operation>} Result of the transformation.
*/
/* harmony default export */ var operation_transform = (transform_transform);
const ot = {
InsertOperation: {
// Transforms InsertOperation `a` by InsertOperation `b`. Accepts a flag stating whether `a` is more important
// than `b` when it comes to resolving conflicts. Returns results as an array of operations.
InsertOperation( a, b, context ) {
// Transformed operations are always new instances, not references to the original operations.
const transformed = a.clone();
// Check whether there is a forced order of nodes or use `context.isStrong` flag for conflict resolving.
const insertBefore = context.insertBefore === undefined ? !context.isStrong : context.insertBefore;
// Transform insert position by the other operation position.
transformed.position = transformed.position._getTransformedByInsertion( b.position, b.nodes.maxOffset, insertBefore );
return [ transformed ];
},
AttributeOperation: doNotUpdate,
RootAttributeOperation: doNotUpdate,
RenameOperation: doNotUpdate,
MarkerOperation: doNotUpdate,
// Transforms InsertOperation `a` by MoveOperation `b`. Accepts a flag stating whether `a` is more important
// than `b` when it comes to resolving conflicts. Returns results as an array of operations.
MoveOperation( a, b, context ) {
const transformed = a.clone();
// Check whether there is a forced order of nodes or use `context.isStrong` flag for conflict resolving.
const insertBefore = context.insertBefore === undefined ? !context.isStrong : context.insertBefore;
// Transform insert position by the other operation parameters.
transformed.position = a.position._getTransformedByMove(
b.sourcePosition,
b.targetPosition,
b.howMany,
insertBefore,
b.isSticky && !context.forceNotSticky
);
return [ transformed ];
}
},
AttributeOperation: {
// Transforms AttributeOperation `a` by InsertOperation `b`. Returns results as an array of operations.
InsertOperation( a, b ) {
// Transform this operation's range.
const ranges = a.range._getTransformedByInsertion( b.position, b.nodes.maxOffset, true, false );
// Map transformed range(s) to operations and return them.
return ranges.reverse().map( range => {
return new attributeoperation_AttributeOperation( range, a.key, a.oldValue, a.newValue, a.baseVersion );
} );
},
// Transforms AttributeOperation `a` by AttributeOperation `b`. Accepts a flag stating whether `a` is more important
// than `b` when it comes to resolving conflicts. Returns results as an array of operations.
AttributeOperation( a, b, context ) {
if ( a.key === b.key ) {
// If operations attributes are in conflict, check if their ranges intersect and manage them properly.
// First, we want to apply change to the part of a range that has not been changed by the other operation.
const operations = a.range.getDifference( b.range ).map( range => {
return new attributeoperation_AttributeOperation( range, a.key, a.oldValue, a.newValue, a.baseVersion );
} );
// Then we take care of the common part of ranges.
const common = a.range.getIntersection( b.range );
if ( common ) {
// If this operation is more important, we also want to apply change to the part of the
// original range that has already been changed by the other operation. Since that range
// got changed we also have to update `oldValue`.
if ( context.isStrong ) {
operations.push( new attributeoperation_AttributeOperation( common, b.key, b.newValue, a.newValue, a.baseVersion ) );
} else if ( operations.length === 0 ) {
operations.push( new NoOperation( 0 ) );
}
}
return operations;
} else {
// If operations don't conflict, simply return an array containing just a clone of this operation.
return [ a.clone() ];
}
},
RootAttributeOperation: doNotUpdate,
RenameOperation: doNotUpdate,
MarkerOperation: doNotUpdate,
// Transforms AttributeOperation `a` by MoveOperation `b`. Returns results as an array of operations.
MoveOperation( a, b ) {
// Convert MoveOperation properties into a range.
const rangeB = range_Range.createFromPositionAndShift( b.sourcePosition, b.howMany );
// This will aggregate transformed ranges.
let ranges = [];
// Difference is a part of changed range that is modified by AttributeOperation but is not affected
// by MoveOperation. This can be zero, one or two ranges (if moved range is inside changed range).
// Right now we will make a simplification and join difference ranges and transform them as one. We will cover rangeB later.
const difference = joinRanges( a.range.getDifference( rangeB ) );
// Common is a range of nodes that is affected by MoveOperation. So it got moved to other place.
const common = a.range.getIntersection( rangeB );
if ( difference !== null ) {
// MoveOperation removes nodes from their original position. We acknowledge this by proper transformation.
// Take the start and the end of the range and transform them by deletion of moved nodes.
// Note that if rangeB was inside AttributeOperation range, only difference.end will be transformed.
// This nicely covers the joining simplification we did in the previous step.
difference.start = difference.start._getTransformedByDeletion( b.sourcePosition, b.howMany );
difference.end = difference.end._getTransformedByDeletion( b.sourcePosition, b.howMany );
// MoveOperation pastes nodes into target position. We acknowledge this by proper transformation.
// Note that since we operate on transformed difference range, we should transform by
// previously transformed target position.
// Note that we do not use Position._getTransformedByMove on range boundaries because we need to
// transform by insertion a range as a whole, since newTargetPosition might be inside that range.
ranges = difference._getTransformedByInsertion( b.getMovedRangeStart(), b.howMany, true, false ).reverse();
}
if ( common !== null ) {
// Here we do not need to worry that newTargetPosition is inside moved range, because that
// would mean that the MoveOperation targets into itself, and that is incorrect operation.
// Instead, we calculate the new position of that part of original range.
common.start = common.start._getCombined( b.sourcePosition, b.getMovedRangeStart() );
common.end = common.end._getCombined( b.sourcePosition, b.getMovedRangeStart() );
ranges.push( common );
}
// Map transformed range(s) to operations and return them.
return ranges.map( range => {
return new attributeoperation_AttributeOperation( range, a.key, a.oldValue, a.newValue, a.baseVersion );
} );
}
},
RootAttributeOperation: {
InsertOperation: doNotUpdate,
AttributeOperation: doNotUpdate,
// Transforms RootAttributeOperation `a` by RootAttributeOperation `b`. Accepts a flag stating whether `a` is more important
// than `b` when it comes to resolving conflicts. Returns results as an array of operations.
RootAttributeOperation( a, b, context ) {
if ( a.root === b.root && a.key === b.key ) {
if ( ( a.newValue !== b.newValue && !context.isStrong ) || a.newValue === b.newValue ) {
return [ new NoOperation( a.baseVersion ) ];
}
}
return [ a.clone() ];
},
RenameOperation: doNotUpdate,
MarkerOperation: doNotUpdate,
MoveOperation: doNotUpdate
},
RenameOperation: {
// Transforms RenameOperation `a` by InsertOperation `b`. Returns results as an array of operations.
InsertOperation( a, b ) {
// Clone the operation, we don't want to alter the original operation.
const clone = a.clone();
// Transform this operation's position.
clone.position = clone.position._getTransformedByInsertion( b.position, b.nodes.maxOffset, true );
return [ clone ];
},
AttributeOperation: doNotUpdate,
RootAttributeOperation: doNotUpdate,
// Transforms RenameOperation `a` by RenameOperation `b`. Accepts a flag stating whether `a` is more important
// than `b` when it comes to resolving conflicts. Returns results as an array of operations.
RenameOperation( a, b, context ) {
// Clone the operation, we don't want to alter the original operation.
const clone = a.clone();
if ( a.position.isEqual( b.position ) ) {
if ( context.isStrong ) {
clone.oldName = b.newName;
} else {
return [ new NoOperation( a.baseVersion ) ];
}
}
return [ clone ];
},
MarkerOperation: doNotUpdate,
// Transforms RenameOperation `a` by MoveOperation `b`. Returns results as an array of operations.
MoveOperation( a, b ) {
const clone = a.clone();
const isSticky = clone.position.isEqual( b.sourcePosition );
clone.position = clone.position._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, true, isSticky );
return [ clone ];
}
},
MarkerOperation: {
// Transforms MarkerOperation `a` by InsertOperation `b`. Returns results as an array of operations.
InsertOperation( a, b ) {
// Clone the operation, we don't want to alter the original operation.
const clone = a.clone();
if ( clone.oldRange ) {
clone.oldRange = clone.oldRange._getTransformedByInsertion( b.position, b.nodes.maxOffset, false, false )[ 0 ];
}
if ( clone.newRange ) {
clone.newRange = clone.newRange._getTransformedByInsertion( b.position, b.nodes.maxOffset, false, false )[ 0 ];
}
return [ clone ];
},
AttributeOperation: doNotUpdate,
RootAttributeOperation: doNotUpdate,
RenameOperation: doNotUpdate,
// Transforms MarkerOperation `a` by MarkerOperation `b`. Accepts a flag stating whether `a` is more important
// than `b` when it comes to resolving conflicts. Returns results as an array of operations.
MarkerOperation( a, b, context ) {
// Clone the operation, we don't want to alter the original operation.
const clone = a.clone();
if ( a.name == b.name ) {
if ( context.isStrong ) {
clone.oldRange = b.newRange;
} else {
return [ new NoOperation( a.baseVersion ) ];
}
}
return [ clone ];
},
// Transforms MarkerOperation `a` by MoveOperation `b`. Returns results as an array of operations.
MoveOperation( a, b ) {
// Clone the operation, we don't want to alter the original operation.
const clone = a.clone();
if ( clone.oldRange ) {
const oldRanges = clone.oldRange._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany );
clone.oldRange = range_Range.createFromRanges( oldRanges );
}
if ( clone.newRange ) {
const newRanges = clone.newRange._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany );
clone.newRange = range_Range.createFromRanges( newRanges );
}
return [ clone ];
}
},
MoveOperation: {
// Transforms MoveOperation `a` by InsertOperation `b`. Accepts a flag stating whether `a` is more important
// than `b` when it comes to resolving conflicts. Returns results as an array of operations.
InsertOperation( a, b, context ) {
// Create range from MoveOperation properties and transform it by insertion.
let range = range_Range.createFromPositionAndShift( a.sourcePosition, a.howMany );
const includeB = a.isSticky && !context.forceNotSticky;
range = range._getTransformedByInsertion( b.position, b.nodes.maxOffset, false, includeB )[ 0 ];
// Check whether there is a forced order of nodes or use `context.isStrong` flag for conflict resolving.
const insertBefore = context.insertBefore === undefined ? !context.isStrong : context.insertBefore;
const result = new a.constructor(
range.start,
range.end.offset - range.start.offset,
a.targetPosition._getTransformedByInsertion( b.position, b.nodes.maxOffset, insertBefore ),
a.baseVersion
);
result.isSticky = a.isSticky;
return [ result ];
},
AttributeOperation: doNotUpdate,
RootAttributeOperation: doNotUpdate,
RenameOperation: doNotUpdate,
MarkerOperation: doNotUpdate,
// Transforms MoveOperation `a` by MoveOperation `b`. Accepts a flag stating whether `a` is more important
// than `b` when it comes to resolving conflicts. Returns results as an array of operations.
MoveOperation( a, b, context ) {
//
// Setting and evaluating some variables that will be used in special cases and default algorithm.
//
// Create ranges from `MoveOperations` properties.
const rangeA = range_Range.createFromPositionAndShift( a.sourcePosition, a.howMany );
const rangeB = range_Range.createFromPositionAndShift( b.sourcePosition, b.howMany );
// Assign `context.isStrong` to a different variable, because the value may change during execution of
// this algorithm and we do not want to override original `context.isStrong` that will be used in later transformations.
let isStrong = context.isStrong;
// Whether range moved by operation `b` is includable in operation `a` move range.
// For this, `a` operation has to be sticky (so `b` sticks to the range) and context has to allow stickiness.
const includeB = a.isSticky && !context.forceNotSticky;
// Evaluate new target position for transformed operation.
// Check whether there is a forced order of nodes or use `isStrong` flag for conflict resolving.
const insertBefore = context.insertBefore === undefined ? !isStrong : context.insertBefore;
// `a.targetPosition` could be affected by the `b` operation. We will transform it.
const newTargetPosition = a.targetPosition._getTransformedByMove(
b.sourcePosition,
b.targetPosition,
b.howMany,
insertBefore,
b.isSticky && !context.forceNotSticky
);
//
// Special case #1 + mirror.
//
// Special case when both move operations' target positions are inside nodes that are
// being moved by the other move operation. So in other words, we move ranges into inside of each other.
// This case can't be solved reasonably (on the other hand, it should not happen often).
if ( moveTargetIntoMovedRange( a, b ) && moveTargetIntoMovedRange( b, a ) ) {
// Instead of transforming operation, we return a reverse of the operation that we transform by.
// So when the results of this "transformation" will be applied, `b` MoveOperation will get reversed.
return [ b.getReversed() ];
}
//
// End of special case #1.
//
//
// Special case #2.
//
// Check if `b` operation targets inside `rangeA`. Use stickiness if possible.
const bTargetsToA = rangeA.containsPosition( b.targetPosition ) ||
( rangeA.start.isEqual( b.targetPosition ) && includeB ) ||
( rangeA.end.isEqual( b.targetPosition ) && includeB );
// If `b` targets to `rangeA` and `rangeA` contains `rangeB`, `b` operation has no influence on `a` operation.
// You might say that operation `b` is captured inside operation `a`.
if ( bTargetsToA && rangeA.containsRange( rangeB, true ) ) {
// There is a mini-special case here, where `rangeB` is on other level than `rangeA`. That's why
// we need to transform `a` operation anyway.
rangeA.start = rangeA.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !includeB );
rangeA.end = rangeA.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, includeB );
return makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition, a );
}
//
// Special case #2 mirror.
//
const aTargetsToB = rangeB.containsPosition( a.targetPosition ) ||
( rangeB.start.isEqual( a.targetPosition ) && b.isSticky && !context.forceNotSticky ) ||
( rangeB.end.isEqual( a.targetPosition ) && b.isSticky && !context.forceNotSticky );
if ( aTargetsToB && rangeB.containsRange( rangeA, true ) ) {
// `a` operation is "moved together" with `b` operation.
// Here, just move `rangeA` "inside" `rangeB`.
rangeA.start = rangeA.start._getCombined( b.sourcePosition, b.getMovedRangeStart() );
rangeA.end = rangeA.end._getCombined( b.sourcePosition, b.getMovedRangeStart() );
return makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition, a );
}
//
// End of special case #2.
//
//
// Special case #3 + mirror.
//
// `rangeA` has a node which is an ancestor of `rangeB`. In other words, `rangeB` is inside `rangeA`
// but not on the same tree level. In such case ranges have common part but we have to treat it
// differently, because in such case those ranges are not really conflicting and should be treated like
// two separate ranges. Also we have to discard two difference parts.
const aCompB = compareArrays( a.sourcePosition.getParentPath(), b.sourcePosition.getParentPath() );
if ( aCompB == 'prefix' || aCompB == 'extension' ) {
// Transform `rangeA` by `b` operation and make operation out of it, and that's all.
// Note that this is a simplified version of default case, but here we treat the common part (whole `rangeA`)
// like a one difference part.
rangeA.start = rangeA.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !includeB );
rangeA.end = rangeA.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, includeB );
return makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition, a );
}
//
// End of special case #3.
//
//
// Default case - ranges are on the same level or are not connected with each other.
//
// Modifier for default case.
// Modifies `isStrong` flag in certain conditions.
//
// If only one of operations is a remove operation, we force remove operation to be the "stronger" one
// to provide more expected results. This is done only if `context.forceWeakRemove` is set to `false`.
// `context.forceWeakRemove` is set to `true` in certain conditions when transformation takes place during undo.
if ( !context.forceWeakRemove ) {
if ( a instanceof removeoperation_RemoveOperation && !( b instanceof removeoperation_RemoveOperation ) ) {
isStrong = true;
} else if ( !( a instanceof removeoperation_RemoveOperation ) && b instanceof removeoperation_RemoveOperation ) {
isStrong = false;
}
}
// Handle operation's source ranges - check how `rangeA` is affected by `b` operation.
// This will aggregate transformed ranges.
const ranges = [];
// Get the "difference part" of `a` operation source range.
// This is an array with one or two ranges. Two ranges if `rangeB` is inside `rangeA`.
const difference = rangeA.getDifference( rangeB );
for ( const range of difference ) {
// Transform those ranges by `b` operation. For example if `b` moved range from before those ranges, fix those ranges.
range.start = range.start._getTransformedByDeletion( b.sourcePosition, b.howMany );
range.end = range.end._getTransformedByDeletion( b.sourcePosition, b.howMany );
// If `b` operation targets into `rangeA` on the same level, spread `rangeA` into two ranges.
const shouldSpread = compareArrays( range.start.getParentPath(), b.getMovedRangeStart().getParentPath() ) == 'same';
const newRanges = range._getTransformedByInsertion( b.getMovedRangeStart(), b.howMany, shouldSpread, includeB );
ranges.push( ...newRanges );
}
// Then, we have to manage the "common part" of both move ranges.
const common = rangeA.getIntersection( rangeB );
if ( common !== null && isStrong && !bTargetsToA ) {
// Calculate the new position of that part of original range.
common.start = common.start._getCombined( b.sourcePosition, b.getMovedRangeStart() );
common.end = common.end._getCombined( b.sourcePosition, b.getMovedRangeStart() );
// Take care of proper range order.
//
// Put `common` at appropriate place. Keep in mind that we are interested in original order.
// Basically there are only three cases: there is zero, one or two difference ranges.
//
// If there is zero difference ranges, just push `common` in the array.
if ( ranges.length === 0 ) {
ranges.push( common );
}
// If there is one difference range, we need to check whether common part was before it or after it.
else if ( ranges.length == 1 ) {
if ( rangeB.start.isBefore( rangeA.start ) || rangeB.start.isEqual( rangeA.start ) ) {
ranges.unshift( common );
} else {
ranges.push( common );
}
}
// If there are more ranges (which means two), put common part between them. This is the only scenario
// where there could be two difference ranges so we don't have to make any comparisons.
else {
ranges.splice( 1, 0, common );
}
}
if ( ranges.length === 0 ) {
// If there are no "source ranges", nothing should be changed.
// Note that this can happen only if `isStrong == false` and `rangeA.isEqual( rangeB )`.
return [ new NoOperation( a.baseVersion ) ];
}
return makeMoveOperationsFromRanges( ranges, newTargetPosition, a );
}
}
};
function transform_transform( a, b, context = { isStrong: false } ) {
let group, algorithm;
if ( a instanceof insertoperation_InsertOperation ) {
group = ot.InsertOperation;
} else if ( a instanceof attributeoperation_AttributeOperation ) {
group = ot.AttributeOperation;
} else if ( a instanceof rootattributeoperation_RootAttributeOperation ) {
group = ot.RootAttributeOperation;
} else if ( a instanceof renameoperation_RenameOperation ) {
group = ot.RenameOperation;
} else if ( a instanceof markeroperation_MarkerOperation ) {
group = ot.MarkerOperation;
} else if ( a instanceof moveoperation_MoveOperation ) {
group = ot.MoveOperation;
} else {
algorithm = doNotUpdate;
}
if ( group ) {
if ( b instanceof insertoperation_InsertOperation ) {
algorithm = group.InsertOperation;
} else if ( b instanceof attributeoperation_AttributeOperation ) {
algorithm = group.AttributeOperation;
} else if ( b instanceof rootattributeoperation_RootAttributeOperation ) {
algorithm = group.RootAttributeOperation;
} else if ( b instanceof renameoperation_RenameOperation ) {
algorithm = group.RenameOperation;
} else if ( b instanceof markeroperation_MarkerOperation ) {
algorithm = group.MarkerOperation;
} else if ( b instanceof moveoperation_MoveOperation ) {
algorithm = group.MoveOperation;
} else {
algorithm = doNotUpdate;
}
}
const transformed = algorithm( a, b, context );
return updateBaseVersions( a.baseVersion, transformed );
}
// When we don't want to update an operation, we create and return a clone of it.
// Returns the operation in "unified format" - wrapped in an Array.
function doNotUpdate( operation ) {
return [ operation.clone() ];
}
// Takes an Array of operations and sets consecutive base versions for them, starting from given base version.
// Returns the passed array.
function updateBaseVersions( baseVersion, operations ) {
for ( let i = 0; i < operations.length; i++ ) {
operations[ i ].baseVersion = baseVersion + i + 1;
}
return operations;
}
// Checks whether MoveOperation targetPosition is inside a node from the moved range of the other MoveOperation.
function moveTargetIntoMovedRange( a, b ) {
return a.targetPosition._getTransformedByDeletion( b.sourcePosition, b.howMany ) === null;
}
// Gets an array of Ranges and produces one Range out of it. The root of a new range will be same as
// the root of the first range in the array. If any of given ranges has different root than the first range,
// it will be discarded.
function joinRanges( ranges ) {
if ( ranges.length === 0 ) {
return null;
} else if ( ranges.length == 1 ) {
return ranges[ 0 ];
} else {
ranges[ 0 ].end = ranges[ ranges.length - 1 ].end;
return ranges[ 0 ];
}
}
// Helper function for `MoveOperation` x `MoveOperation` transformation.
// Convert given ranges and target position to move operations and return them.
// Ranges and target position will be transformed on-the-fly when generating operations.
// Given `ranges` should be in the order of how they were in the original transformed operation.
// Given `targetPosition` is the target position of the first range from `ranges`.
function makeMoveOperationsFromRanges( ranges, targetPosition, a ) {
// At this moment we have some ranges and a target position, to which those ranges should be moved.
// Order in `ranges` array is the go-to order of after transformation.
//
// We are almost done. We have `ranges` and `targetPosition` to make operations from.
// Unfortunately, those operations may affect each other. Precisely, first operation after move
// may affect source range and target position of second and third operation. Same with second
// operation affecting third.
//
// We need to fix those source ranges and target positions once again, before converting `ranges` to operations.
const operations = [];
// Keep in mind that nothing will be transformed if there is just one range in `ranges`.
for ( let i = 0; i < ranges.length; i++ ) {
// Create new operation out of a range and target position.
const op = makeMoveOperation( ranges[ i ], targetPosition, a.isSticky );
operations.push( op );
// Transform other ranges by the generated operation.
for ( let j = i + 1; j < ranges.length; j++ ) {
// All ranges in `ranges` array should be:
// * non-intersecting (these are part of original operation source range), and
// * `targetPosition` does not target into them (opposite would mean that transformed operation targets "inside itself").
//
// This means that the transformation will be "clean" and always return one result.
ranges[ j ] = ranges[ j ]._getTransformedByMove( op.sourcePosition, op.targetPosition, op.howMany )[ 0 ];
}
targetPosition = targetPosition._getTransformedByMove( op.sourcePosition, op.targetPosition, op.howMany, true, false );
}
return operations;
}
function makeMoveOperation( range, targetPosition, isSticky ) {
// We want to keep correct operation class.
let OperationClass;
if ( targetPosition.root.rootName == '$graveyard' ) {
OperationClass = removeoperation_RemoveOperation;
} else if ( range.start.root.rootName == '$graveyard' ) {
OperationClass = reinsertoperation_ReinsertOperation;
} else {
OperationClass = moveoperation_MoveOperation;
}
const result = new OperationClass(
range.start,
range.end.offset - range.start.offset,
targetPosition,
0 // Is corrected anyway later.
);
result.isSticky = isSticky;
return result;
}
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseSlice.js
/**
* The base implementation of `_.slice` without an iteratee call guard.
*
* @private
* @param {Array} array The array to slice.
* @param {number} [start=0] The start position.
* @param {number} [end=array.length] The end position.
* @returns {Array} Returns the slice of `array`.
*/
function baseSlice(array, start, end) {
var index = -1,
length = array.length;
if (start < 0) {
start = -start > length ? 0 : (length + start);
}
end = end > length ? length : end;
if (end < 0) {
end += length;
}
length = start > end ? 0 : ((end - start) >>> 0);
start >>>= 0;
var result = Array(length);
while (++index < length) {
result[index] = array[index + start];
}
return result;
}
/* harmony default export */ var _baseSlice = (baseSlice);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/chunk.js
/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeCeil = Math.ceil,
chunk_nativeMax = Math.max;
/**
* Creates an array of elements split into groups the length of `size`.
* If `array` can't be split evenly, the final chunk will be the remaining
* elements.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The array to process.
* @param {number} [size=1] The length of each chunk
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {Array} Returns the new array of chunks.
* @example
*
* _.chunk(['a', 'b', 'c', 'd'], 2);
* // => [['a', 'b'], ['c', 'd']]
*
* _.chunk(['a', 'b', 'c', 'd'], 3);
* // => [['a', 'b', 'c'], ['d']]
*/
function chunk(array, size, guard) {
if ((guard ? _isIterateeCall(array, size, guard) : size === undefined)) {
size = 1;
} else {
size = chunk_nativeMax(lodash_toInteger(size), 0);
}
var length = array ? array.length : 0;
if (!length || size < 1) {
return [];
}
var index = 0,
resIndex = 0,
result = Array(nativeCeil(length / size));
while (index < length) {
result[resIndex++] = _baseSlice(array, index, (index += size));
}
return result;
}
/* harmony default export */ var lodash_chunk = (chunk);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/compact.js
/**
* Creates an array with all falsey values removed. The values `false`, `null`,
* `0`, `""`, `undefined`, and `NaN` are falsey.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to compact.
* @returns {Array} Returns the new array of filtered values.
* @example
*
* _.compact([0, 1, false, 2, '', 3]);
* // => [1, 2, 3]
*/
function compact(array) {
var index = -1,
length = array ? array.length : 0,
resIndex = 0,
result = [];
while (++index < length) {
var value = array[index];
if (value) {
result[resIndex++] = value;
}
}
return result;
}
/* harmony default export */ var lodash_compact = (compact);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_isFlattenable.js
/**
* Checks if `value` is a flattenable `arguments` object or array.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is flattenable, else `false`.
*/
function isFlattenable(value) {
return lodash_isArray(value) || lodash_isArguments(value);
}
/* harmony default export */ var _isFlattenable = (isFlattenable);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseFlatten.js
/**
* The base implementation of `_.flatten` with support for restricting flattening.
*
* @private
* @param {Array} array The array to flatten.
* @param {number} depth The maximum recursion depth.
* @param {boolean} [predicate=isFlattenable] The function invoked per iteration.
* @param {boolean} [isStrict] Restrict to values that pass `predicate` checks.
* @param {Array} [result=[]] The initial result value.
* @returns {Array} Returns the new flattened array.
*/
function baseFlatten(array, depth, predicate, isStrict, result) {
var index = -1,
length = array.length;
predicate || (predicate = _isFlattenable);
result || (result = []);
while (++index < length) {
var value = array[index];
if (depth > 0 && predicate(value)) {
if (depth > 1) {
// Recursively flatten arrays (susceptible to call stack limits).
baseFlatten(value, depth - 1, predicate, isStrict, result);
} else {
_arrayPush(result, value);
}
} else if (!isStrict) {
result[result.length] = value;
}
}
return result;
}
/* harmony default export */ var _baseFlatten = (baseFlatten);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/concat.js
/**
* Creates a new array concatenating `array` with any additional arrays
* and/or values.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to concatenate.
* @param {...*} [values] The values to concatenate.
* @returns {Array} Returns the new concatenated array.
* @example
*
* var array = [1];
* var other = _.concat(array, 2, [3], [[4]]);
*
* console.log(other);
* // => [1, 2, 3, [4]]
*
* console.log(array);
* // => [1]
*/
function concat() {
var length = arguments.length,
args = Array(length ? length - 1 : 0),
array = arguments[0],
index = length;
while (index--) {
args[index - 1] = arguments[index];
}
return length
? _arrayPush(lodash_isArray(array) ? _copyArray(array) : [array], _baseFlatten(args, 1))
: [];
}
/* harmony default export */ var lodash_concat = (concat);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_indexOfNaN.js
/**
* Gets the index at which the first occurrence of `NaN` is found in `array`.
*
* @private
* @param {Array} array The array to search.
* @param {number} fromIndex The index to search from.
* @param {boolean} [fromRight] Specify iterating from right to left.
* @returns {number} Returns the index of the matched `NaN`, else `-1`.
*/
function indexOfNaN(array, fromIndex, fromRight) {
var length = array.length,
index = fromIndex + (fromRight ? 0 : -1);
while ((fromRight ? index-- : ++index < length)) {
var other = array[index];
if (other !== other) {
return index;
}
}
return -1;
}
/* harmony default export */ var _indexOfNaN = (indexOfNaN);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseIndexOf.js
/**
* The base implementation of `_.indexOf` without `fromIndex` bounds checks.
*
* @private
* @param {Array} array The array to search.
* @param {*} value The value to search for.
* @param {number} fromIndex The index to search from.
* @returns {number} Returns the index of the matched value, else `-1`.
*/
function baseIndexOf(array, value, fromIndex) {
if (value !== value) {
return _indexOfNaN(array, fromIndex);
}
var index = fromIndex - 1,
length = array.length;
while (++index < length) {
if (array[index] === value) {
return index;
}
}
return -1;
}
/* harmony default export */ var _baseIndexOf = (baseIndexOf);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_arrayIncludes.js
/**
* A specialized version of `_.includes` for arrays without support for
* specifying an index to search from.
*
* @private
* @param {Array} array The array to search.
* @param {*} target The value to search for.
* @returns {boolean} Returns `true` if `target` is found, else `false`.
*/
function arrayIncludes(array, value) {
return !!array.length && _baseIndexOf(array, value, 0) > -1;
}
/* harmony default export */ var _arrayIncludes = (arrayIncludes);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_arrayIncludesWith.js
/**
* This function is like `arrayIncludes` except that it accepts a comparator.
*
* @private
* @param {Array} array The array to search.
* @param {*} target The value to search for.
* @param {Function} comparator The comparator invoked per element.
* @returns {boolean} Returns `true` if `target` is found, else `false`.
*/
function arrayIncludesWith(array, value, comparator) {
var index = -1,
length = array.length;
while (++index < length) {
if (comparator(value, array[index])) {
return true;
}
}
return false;
}
/* harmony default export */ var _arrayIncludesWith = (arrayIncludesWith);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_arrayMap.js
/**
* A specialized version of `_.map` for arrays without support for iteratee
* shorthands.
*
* @private
* @param {Array} array The array to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Array} Returns the new mapped array.
*/
function arrayMap(array, iteratee) {
var index = -1,
length = array.length,
result = Array(length);
while (++index < length) {
result[index] = iteratee(array[index], index, array);
}
return result;
}
/* harmony default export */ var _arrayMap = (arrayMap);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseUnary.js
/**
* The base implementation of `_.unary` without support for storing wrapper metadata.
*
* @private
* @param {Function} func The function to cap arguments for.
* @returns {Function} Returns the new capped function.
*/
function baseUnary(func) {
return function(value) {
return func(value);
};
}
/* harmony default export */ var _baseUnary = (baseUnary);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_cacheHas.js
/**
* Checks if a cache value for `key` exists.
*
* @private
* @param {Object} cache The cache to query.
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function cacheHas(cache, key) {
return cache.has(key);
}
/* harmony default export */ var _cacheHas = (cacheHas);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseDifference.js
/** Used as the size to enable large array optimizations. */
var _baseDifference_LARGE_ARRAY_SIZE = 200;
/**
* The base implementation of methods like `_.difference` without support
* for excluding multiple arrays or iteratee shorthands.
*
* @private
* @param {Array} array The array to inspect.
* @param {Array} values The values to exclude.
* @param {Function} [iteratee] The iteratee invoked per element.
* @param {Function} [comparator] The comparator invoked per element.
* @returns {Array} Returns the new array of filtered values.
*/
function baseDifference(array, values, iteratee, comparator) {
var index = -1,
includes = _arrayIncludes,
isCommon = true,
length = array.length,
result = [],
valuesLength = values.length;
if (!length) {
return result;
}
if (iteratee) {
values = _arrayMap(values, _baseUnary(iteratee));
}
if (comparator) {
includes = _arrayIncludesWith;
isCommon = false;
}
else if (values.length >= _baseDifference_LARGE_ARRAY_SIZE) {
includes = _cacheHas;
isCommon = false;
values = new _SetCache(values);
}
outer:
while (++index < length) {
var value = array[index],
computed = iteratee ? iteratee(value) : value;
value = (comparator || value !== 0) ? value : 0;
if (isCommon && computed === computed) {
var valuesIndex = valuesLength;
while (valuesIndex--) {
if (values[valuesIndex] === computed) {
continue outer;
}
}
result.push(value);
}
else if (!includes(values, computed, comparator)) {
result.push(value);
}
}
return result;
}
/* harmony default export */ var _baseDifference = (baseDifference);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/difference.js
/**
* Creates an array of unique `array` values not included in the other given
* arrays using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
* for equality comparisons. The order of result values is determined by the
* order they occur in the first array.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to inspect.
* @param {...Array} [values] The values to exclude.
* @returns {Array} Returns the new array of filtered values.
* @see _.without, _.xor
* @example
*
* _.difference([3, 2, 1], [4, 2]);
* // => [3, 1]
*/
var difference_difference = lodash_rest(function(array, values) {
return lodash_isArrayLikeObject(array)
? _baseDifference(array, _baseFlatten(values, 1, lodash_isArrayLikeObject, true))
: [];
});
/* harmony default export */ var lodash_difference = (difference_difference);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseIsMatch.js
/** Used to compose bitmasks for comparison styles. */
var _baseIsMatch_UNORDERED_COMPARE_FLAG = 1,
_baseIsMatch_PARTIAL_COMPARE_FLAG = 2;
/**
* The base implementation of `_.isMatch` without support for iteratee shorthands.
*
* @private
* @param {Object} object The object to inspect.
* @param {Object} source The object of property values to match.
* @param {Array} matchData The property names, values, and compare flags to match.
* @param {Function} [customizer] The function to customize comparisons.
* @returns {boolean} Returns `true` if `object` is a match, else `false`.
*/
function baseIsMatch(object, source, matchData, customizer) {
var index = matchData.length,
length = index,
noCustomizer = !customizer;
if (object == null) {
return !length;
}
object = Object(object);
while (index--) {
var data = matchData[index];
if ((noCustomizer && data[2])
? data[1] !== object[data[0]]
: !(data[0] in object)
) {
return false;
}
}
while (++index < length) {
data = matchData[index];
var key = data[0],
objValue = object[key],
srcValue = data[1];
if (noCustomizer && data[2]) {
if (objValue === undefined && !(key in object)) {
return false;
}
} else {
var stack = new _Stack;
if (customizer) {
var result = customizer(objValue, srcValue, key, object, source, stack);
}
if (!(result === undefined
? _baseIsEqual(srcValue, objValue, customizer, _baseIsMatch_UNORDERED_COMPARE_FLAG | _baseIsMatch_PARTIAL_COMPARE_FLAG, stack)
: result
)) {
return false;
}
}
}
return true;
}
/* harmony default export */ var _baseIsMatch = (baseIsMatch);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_isStrictComparable.js
/**
* Checks if `value` is suitable for strict equality comparisons, i.e. `===`.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` if suitable for strict
* equality comparisons, else `false`.
*/
function isStrictComparable(value) {
return value === value && !lodash_isObject(value);
}
/* harmony default export */ var _isStrictComparable = (isStrictComparable);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseToPairs.js
/**
* The base implementation of `_.toPairs` and `_.toPairsIn` which creates an array
* of key-value pairs for `object` corresponding to the property names of `props`.
*
* @private
* @param {Object} object The object to query.
* @param {Array} props The property names to get values for.
* @returns {Object} Returns the key-value pairs.
*/
function baseToPairs(object, props) {
return _arrayMap(props, function(key) {
return [key, object[key]];
});
}
/* harmony default export */ var _baseToPairs = (baseToPairs);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_setToPairs.js
/**
* Converts `set` to its value-value pairs.
*
* @private
* @param {Object} set The set to convert.
* @returns {Array} Returns the value-value pairs.
*/
function setToPairs(set) {
var index = -1,
result = Array(set.size);
set.forEach(function(value) {
result[++index] = [value, value];
});
return result;
}
/* harmony default export */ var _setToPairs = (setToPairs);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_createToPairs.js
/** `Object#toString` result references. */
var _createToPairs_mapTag = '[object Map]',
_createToPairs_setTag = '[object Set]';
/**
* Creates a `_.toPairs` or `_.toPairsIn` function.
*
* @private
* @param {Function} keysFunc The function to get the keys of a given object.
* @returns {Function} Returns the new pairs function.
*/
function createToPairs(keysFunc) {
return function(object) {
var tag = _getTag(object);
if (tag == _createToPairs_mapTag) {
return _mapToArray(object);
}
if (tag == _createToPairs_setTag) {
return _setToPairs(object);
}
return _baseToPairs(object, keysFunc(object));
};
}
/* harmony default export */ var _createToPairs = (createToPairs);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/toPairs.js
/**
* Creates an array of own enumerable string keyed-value pairs for `object`
* which can be consumed by `_.fromPairs`. If `object` is a map or set, its
* entries are returned.
*
* @static
* @memberOf _
* @since 4.0.0
* @alias entries
* @category Object
* @param {Object} object The object to query.
* @returns {Array} Returns the key-value pairs.
* @example
*
* function Foo() {
* this.a = 1;
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.toPairs(new Foo);
* // => [['a', 1], ['b', 2]] (iteration order is not guaranteed)
*/
var toPairs = _createToPairs(lodash_keys);
/* harmony default export */ var lodash_toPairs = (toPairs);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_getMatchData.js
/**
* Gets the property names, values, and compare flags of `object`.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the match data of `object`.
*/
function getMatchData(object) {
var result = lodash_toPairs(object),
length = result.length;
while (length--) {
result[length][2] = _isStrictComparable(result[length][1]);
}
return result;
}
/* harmony default export */ var _getMatchData = (getMatchData);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_matchesStrictComparable.js
/**
* A specialized version of `matchesProperty` for source values suitable
* for strict equality comparisons, i.e. `===`.
*
* @private
* @param {string} key The key of the property to get.
* @param {*} srcValue The value to match.
* @returns {Function} Returns the new spec function.
*/
function matchesStrictComparable(key, srcValue) {
return function(object) {
if (object == null) {
return false;
}
return object[key] === srcValue &&
(srcValue !== undefined || (key in Object(object)));
};
}
/* harmony default export */ var _matchesStrictComparable = (matchesStrictComparable);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseMatches.js
/**
* The base implementation of `_.matches` which doesn't clone `source`.
*
* @private
* @param {Object} source The object of property values to match.
* @returns {Function} Returns the new spec function.
*/
function baseMatches(source) {
var matchData = _getMatchData(source);
if (matchData.length == 1 && matchData[0][2]) {
return _matchesStrictComparable(matchData[0][0], matchData[0][1]);
}
return function(object) {
return object === source || _baseIsMatch(object, source, matchData);
};
}
/* harmony default export */ var _baseMatches = (baseMatches);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/memoize.js
/** Used as the `TypeError` message for "Functions" methods. */
var memoize_FUNC_ERROR_TEXT = 'Expected a function';
/**
* Creates a function that memoizes the result of `func`. If `resolver` is
* provided, it determines the cache key for storing the result based on the
* arguments provided to the memoized function. By default, the first argument
* provided to the memoized function is used as the map cache key. The `func`
* is invoked with the `this` binding of the memoized function.
*
* **Note:** The cache is exposed as the `cache` property on the memoized
* function. Its creation may be customized by replacing the `_.memoize.Cache`
* constructor with one whose instances implement the
* [`Map`](http://ecma-international.org/ecma-262/6.0/#sec-properties-of-the-map-prototype-object)
* method interface of `delete`, `get`, `has`, and `set`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Function
* @param {Function} func The function to have its output memoized.
* @param {Function} [resolver] The function to resolve the cache key.
* @returns {Function} Returns the new memoized function.
* @example
*
* var object = { 'a': 1, 'b': 2 };
* var other = { 'c': 3, 'd': 4 };
*
* var values = _.memoize(_.values);
* values(object);
* // => [1, 2]
*
* values(other);
* // => [3, 4]
*
* object.a = 2;
* values(object);
* // => [1, 2]
*
* // Modify the result cache.
* values.cache.set(object, ['a', 'b']);
* values(object);
* // => ['a', 'b']
*
* // Replace `_.memoize.Cache`.
* _.memoize.Cache = WeakMap;
*/
function memoize(func, resolver) {
if (typeof func != 'function' || (resolver && typeof resolver != 'function')) {
throw new TypeError(memoize_FUNC_ERROR_TEXT);
}
var memoized = function() {
var args = arguments,
key = resolver ? resolver.apply(this, args) : args[0],
cache = memoized.cache;
if (cache.has(key)) {
return cache.get(key);
}
var result = func.apply(this, args);
memoized.cache = cache.set(key, result);
return result;
};
memoized.cache = new (memoize.Cache || _MapCache);
return memoized;
}
// Assign cache to `_.memoize`.
memoize.Cache = _MapCache;
/* harmony default export */ var lodash_memoize = (memoize);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseToString.js
/** Used as references for various `Number` constants. */
var _baseToString_INFINITY = 1 / 0;
/** Used to convert symbols to primitives and strings. */
var _baseToString_symbolProto = _Symbol ? _Symbol.prototype : undefined,
symbolToString = _baseToString_symbolProto ? _baseToString_symbolProto.toString : undefined;
/**
* The base implementation of `_.toString` which doesn't convert nullish
* values to empty strings.
*
* @private
* @param {*} value The value to process.
* @returns {string} Returns the string.
*/
function baseToString(value) {
// Exit early for strings to avoid a performance hit in some environments.
if (typeof value == 'string') {
return value;
}
if (lodash_isSymbol(value)) {
return symbolToString ? symbolToString.call(value) : '';
}
var result = (value + '');
return (result == '0' && (1 / value) == -_baseToString_INFINITY) ? '-0' : result;
}
/* harmony default export */ var _baseToString = (baseToString);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/toString.js
/**
* Converts `value` to a string. An empty string is returned for `null`
* and `undefined` values. The sign of `-0` is preserved.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to process.
* @returns {string} Returns the string.
* @example
*
* _.toString(null);
* // => ''
*
* _.toString(-0);
* // => '-0'
*
* _.toString([1, 2, 3]);
* // => '1,2,3'
*/
function toString_toString(value) {
return value == null ? '' : _baseToString(value);
}
/* harmony default export */ var lodash_toString = (toString_toString);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_stringToPath.js
/** Used to match property names within property paths. */
var rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]/g;
/** Used to match backslashes in property paths. */
var reEscapeChar = /\\(\\)?/g;
/**
* Converts `string` to a property path array.
*
* @private
* @param {string} string The string to convert.
* @returns {Array} Returns the property path array.
*/
var stringToPath = lodash_memoize(function(string) {
var result = [];
lodash_toString(string).replace(rePropName, function(match, number, quote, string) {
result.push(quote ? string.replace(reEscapeChar, '$1') : (number || match));
});
return result;
});
/* harmony default export */ var _stringToPath = (stringToPath);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_castPath.js
/**
* Casts `value` to a path array if it's not one.
*
* @private
* @param {*} value The value to inspect.
* @returns {Array} Returns the cast property path array.
*/
function castPath(value) {
return lodash_isArray(value) ? value : _stringToPath(value);
}
/* harmony default export */ var _castPath = (castPath);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_isKey.js
/** Used to match property names within property paths. */
var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,
reIsPlainProp = /^\w*$/;
/**
* Checks if `value` is a property name and not a property path.
*
* @private
* @param {*} value The value to check.
* @param {Object} [object] The object to query keys on.
* @returns {boolean} Returns `true` if `value` is a property name, else `false`.
*/
function isKey(value, object) {
if (lodash_isArray(value)) {
return false;
}
var type = typeof value;
if (type == 'number' || type == 'symbol' || type == 'boolean' ||
value == null || lodash_isSymbol(value)) {
return true;
}
return reIsPlainProp.test(value) || !reIsDeepProp.test(value) ||
(object != null && value in Object(object));
}
/* harmony default export */ var _isKey = (isKey);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_toKey.js
/** Used as references for various `Number` constants. */
var _toKey_INFINITY = 1 / 0;
/**
* Converts `value` to a string key if it's not a string or symbol.
*
* @private
* @param {*} value The value to inspect.
* @returns {string|symbol} Returns the key.
*/
function toKey(value) {
if (typeof value == 'string' || lodash_isSymbol(value)) {
return value;
}
var result = (value + '');
return (result == '0' && (1 / value) == -_toKey_INFINITY) ? '-0' : result;
}
/* harmony default export */ var _toKey = (toKey);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseGet.js
/**
* The base implementation of `_.get` without support for default values.
*
* @private
* @param {Object} object The object to query.
* @param {Array|string} path The path of the property to get.
* @returns {*} Returns the resolved value.
*/
function baseGet(object, path) {
path = _isKey(path, object) ? [path] : _castPath(path);
var index = 0,
length = path.length;
while (object != null && index < length) {
object = object[_toKey(path[index++])];
}
return (index && index == length) ? object : undefined;
}
/* harmony default export */ var _baseGet = (baseGet);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/get.js
/**
* Gets the value at `path` of `object`. If the resolved value is
* `undefined`, the `defaultValue` is used in its place.
*
* @static
* @memberOf _
* @since 3.7.0
* @category Object
* @param {Object} object The object to query.
* @param {Array|string} path The path of the property to get.
* @param {*} [defaultValue] The value returned for `undefined` resolved values.
* @returns {*} Returns the resolved value.
* @example
*
* var object = { 'a': [{ 'b': { 'c': 3 } }] };
*
* _.get(object, 'a[0].b.c');
* // => 3
*
* _.get(object, ['a', '0', 'b', 'c']);
* // => 3
*
* _.get(object, 'a.b.c', 'default');
* // => 'default'
*/
function get(object, path, defaultValue) {
var result = object == null ? undefined : _baseGet(object, path);
return result === undefined ? defaultValue : result;
}
/* harmony default export */ var lodash_get = (get);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseHasIn.js
/**
* The base implementation of `_.hasIn` without support for deep paths.
*
* @private
* @param {Object} object The object to query.
* @param {Array|string} key The key to check.
* @returns {boolean} Returns `true` if `key` exists, else `false`.
*/
function baseHasIn(object, key) {
return key in Object(object);
}
/* harmony default export */ var _baseHasIn = (baseHasIn);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_hasPath.js
/**
* Checks if `path` exists on `object`.
*
* @private
* @param {Object} object The object to query.
* @param {Array|string} path The path to check.
* @param {Function} hasFunc The function to check properties.
* @returns {boolean} Returns `true` if `path` exists, else `false`.
*/
function hasPath(object, path, hasFunc) {
path = _isKey(path, object) ? [path] : _castPath(path);
var result,
index = -1,
length = path.length;
while (++index < length) {
var key = _toKey(path[index]);
if (!(result = object != null && hasFunc(object, key))) {
break;
}
object = object[key];
}
if (result) {
return result;
}
var length = object ? object.length : 0;
return !!length && lodash_isLength(length) && _isIndex(key, length) &&
(lodash_isArray(object) || lodash_isString(object) || lodash_isArguments(object));
}
/* harmony default export */ var _hasPath = (hasPath);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/hasIn.js
/**
* Checks if `path` is a direct or inherited property of `object`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Object
* @param {Object} object The object to query.
* @param {Array|string} path The path to check.
* @returns {boolean} Returns `true` if `path` exists, else `false`.
* @example
*
* var object = _.create({ 'a': _.create({ 'b': 2 }) });
*
* _.hasIn(object, 'a');
* // => true
*
* _.hasIn(object, 'a.b');
* // => true
*
* _.hasIn(object, ['a', 'b']);
* // => true
*
* _.hasIn(object, 'b');
* // => false
*/
function hasIn(object, path) {
return object != null && _hasPath(object, path, _baseHasIn);
}
/* harmony default export */ var lodash_hasIn = (hasIn);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseMatchesProperty.js
/** Used to compose bitmasks for comparison styles. */
var _baseMatchesProperty_UNORDERED_COMPARE_FLAG = 1,
_baseMatchesProperty_PARTIAL_COMPARE_FLAG = 2;
/**
* The base implementation of `_.matchesProperty` which doesn't clone `srcValue`.
*
* @private
* @param {string} path The path of the property to get.
* @param {*} srcValue The value to match.
* @returns {Function} Returns the new spec function.
*/
function baseMatchesProperty(path, srcValue) {
if (_isKey(path) && _isStrictComparable(srcValue)) {
return _matchesStrictComparable(_toKey(path), srcValue);
}
return function(object) {
var objValue = lodash_get(object, path);
return (objValue === undefined && objValue === srcValue)
? lodash_hasIn(object, path)
: _baseIsEqual(srcValue, objValue, undefined, _baseMatchesProperty_UNORDERED_COMPARE_FLAG | _baseMatchesProperty_PARTIAL_COMPARE_FLAG);
};
}
/* harmony default export */ var _baseMatchesProperty = (baseMatchesProperty);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/identity.js
/**
* This method returns the first argument given to it.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Util
* @param {*} value Any value.
* @returns {*} Returns `value`.
* @example
*
* var object = { 'user': 'fred' };
*
* _.identity(object) === object;
* // => true
*/
function identity(value) {
return value;
}
/* harmony default export */ var lodash_identity = (identity);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_basePropertyDeep.js
/**
* A specialized version of `baseProperty` which supports deep paths.
*
* @private
* @param {Array|string} path The path of the property to get.
* @returns {Function} Returns the new accessor function.
*/
function basePropertyDeep(path) {
return function(object) {
return _baseGet(object, path);
};
}
/* harmony default export */ var _basePropertyDeep = (basePropertyDeep);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/property.js
/**
* Creates a function that returns the value at `path` of a given object.
*
* @static
* @memberOf _
* @since 2.4.0
* @category Util
* @param {Array|string} path The path of the property to get.
* @returns {Function} Returns the new accessor function.
* @example
*
* var objects = [
* { 'a': { 'b': 2 } },
* { 'a': { 'b': 1 } }
* ];
*
* _.map(objects, _.property('a.b'));
* // => [2, 1]
*
* _.map(_.sortBy(objects, _.property(['a', 'b'])), 'a.b');
* // => [1, 2]
*/
function property_property(path) {
return _isKey(path) ? _baseProperty(_toKey(path)) : _basePropertyDeep(path);
}
/* harmony default export */ var lodash_property = (property_property);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseIteratee.js
/**
* The base implementation of `_.iteratee`.
*
* @private
* @param {*} [value=_.identity] The value to convert to an iteratee.
* @returns {Function} Returns the iteratee.
*/
function baseIteratee(value) {
// Don't store the `typeof` result in a variable to avoid a JIT bug in Safari 9.
// See https://bugs.webkit.org/show_bug.cgi?id=156034 for more details.
if (typeof value == 'function') {
return value;
}
if (value == null) {
return lodash_identity;
}
if (typeof value == 'object') {
return lodash_isArray(value)
? _baseMatchesProperty(value[0], value[1])
: _baseMatches(value);
}
return lodash_property(value);
}
/* harmony default export */ var _baseIteratee = (baseIteratee);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/differenceBy.js
/**
* This method is like `_.difference` except that it accepts `iteratee` which
* is invoked for each element of `array` and `values` to generate the criterion
* by which they're compared. Result values are chosen from the first array.
* The iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to inspect.
* @param {...Array} [values] The values to exclude.
* @param {Array|Function|Object|string} [iteratee=_.identity]
* The iteratee invoked per element.
* @returns {Array} Returns the new array of filtered values.
* @example
*
* _.differenceBy([3.1, 2.2, 1.3], [4.4, 2.5], Math.floor);
* // => [3.1, 1.3]
*
* // The `_.property` iteratee shorthand.
* _.differenceBy([{ 'x': 2 }, { 'x': 1 }], [{ 'x': 1 }], 'x');
* // => [{ 'x': 2 }]
*/
var differenceBy = lodash_rest(function(array, values) {
var iteratee = lodash_last(values);
if (lodash_isArrayLikeObject(iteratee)) {
iteratee = undefined;
}
return lodash_isArrayLikeObject(array)
? _baseDifference(array, _baseFlatten(values, 1, lodash_isArrayLikeObject, true), _baseIteratee(iteratee))
: [];
});
/* harmony default export */ var lodash_differenceBy = (differenceBy);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/differenceWith.js
/**
* This method is like `_.difference` except that it accepts `comparator`
* which is invoked to compare elements of `array` to `values`. Result values
* are chosen from the first array. The comparator is invoked with two arguments:
* (arrVal, othVal).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to inspect.
* @param {...Array} [values] The values to exclude.
* @param {Function} [comparator] The comparator invoked per element.
* @returns {Array} Returns the new array of filtered values.
* @example
*
* var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];
*
* _.differenceWith(objects, [{ 'x': 1, 'y': 2 }], _.isEqual);
* // => [{ 'x': 2, 'y': 1 }]
*/
var differenceWith = lodash_rest(function(array, values) {
var comparator = lodash_last(values);
if (lodash_isArrayLikeObject(comparator)) {
comparator = undefined;
}
return lodash_isArrayLikeObject(array)
? _baseDifference(array, _baseFlatten(values, 1, lodash_isArrayLikeObject, true), undefined, comparator)
: [];
});
/* harmony default export */ var lodash_differenceWith = (differenceWith);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/drop.js
/**
* Creates a slice of `array` with `n` elements dropped from the beginning.
*
* @static
* @memberOf _
* @since 0.5.0
* @category Array
* @param {Array} array The array to query.
* @param {number} [n=1] The number of elements to drop.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {Array} Returns the slice of `array`.
* @example
*
* _.drop([1, 2, 3]);
* // => [2, 3]
*
* _.drop([1, 2, 3], 2);
* // => [3]
*
* _.drop([1, 2, 3], 5);
* // => []
*
* _.drop([1, 2, 3], 0);
* // => [1, 2, 3]
*/
function drop(array, n, guard) {
var length = array ? array.length : 0;
if (!length) {
return [];
}
n = (guard || n === undefined) ? 1 : lodash_toInteger(n);
return _baseSlice(array, n < 0 ? 0 : n, length);
}
/* harmony default export */ var lodash_drop = (drop);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/dropRight.js
/**
* Creates a slice of `array` with `n` elements dropped from the end.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The array to query.
* @param {number} [n=1] The number of elements to drop.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {Array} Returns the slice of `array`.
* @example
*
* _.dropRight([1, 2, 3]);
* // => [1, 2]
*
* _.dropRight([1, 2, 3], 2);
* // => [1]
*
* _.dropRight([1, 2, 3], 5);
* // => []
*
* _.dropRight([1, 2, 3], 0);
* // => [1, 2, 3]
*/
function dropRight(array, n, guard) {
var length = array ? array.length : 0;
if (!length) {
return [];
}
n = (guard || n === undefined) ? 1 : lodash_toInteger(n);
n = length - n;
return _baseSlice(array, 0, n < 0 ? 0 : n);
}
/* harmony default export */ var lodash_dropRight = (dropRight);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseWhile.js
/**
* The base implementation of methods like `_.dropWhile` and `_.takeWhile`
* without support for iteratee shorthands.
*
* @private
* @param {Array} array The array to query.
* @param {Function} predicate The function invoked per iteration.
* @param {boolean} [isDrop] Specify dropping elements instead of taking them.
* @param {boolean} [fromRight] Specify iterating from right to left.
* @returns {Array} Returns the slice of `array`.
*/
function baseWhile(array, predicate, isDrop, fromRight) {
var length = array.length,
index = fromRight ? length : -1;
while ((fromRight ? index-- : ++index < length) &&
predicate(array[index], index, array)) {}
return isDrop
? _baseSlice(array, (fromRight ? 0 : index), (fromRight ? index + 1 : length))
: _baseSlice(array, (fromRight ? index + 1 : 0), (fromRight ? length : index));
}
/* harmony default export */ var _baseWhile = (baseWhile);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/dropRightWhile.js
/**
* Creates a slice of `array` excluding elements dropped from the end.
* Elements are dropped until `predicate` returns falsey. The predicate is
* invoked with three arguments: (value, index, array).
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The array to query.
* @param {Array|Function|Object|string} [predicate=_.identity]
* The function invoked per iteration.
* @returns {Array} Returns the slice of `array`.
* @example
*
* var users = [
* { 'user': 'barney', 'active': true },
* { 'user': 'fred', 'active': false },
* { 'user': 'pebbles', 'active': false }
* ];
*
* _.dropRightWhile(users, function(o) { return !o.active; });
* // => objects for ['barney']
*
* // The `_.matches` iteratee shorthand.
* _.dropRightWhile(users, { 'user': 'pebbles', 'active': false });
* // => objects for ['barney', 'fred']
*
* // The `_.matchesProperty` iteratee shorthand.
* _.dropRightWhile(users, ['active', false]);
* // => objects for ['barney']
*
* // The `_.property` iteratee shorthand.
* _.dropRightWhile(users, 'active');
* // => objects for ['barney', 'fred', 'pebbles']
*/
function dropRightWhile(array, predicate) {
return (array && array.length)
? _baseWhile(array, _baseIteratee(predicate, 3), true, true)
: [];
}
/* harmony default export */ var lodash_dropRightWhile = (dropRightWhile);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/dropWhile.js
/**
* Creates a slice of `array` excluding elements dropped from the beginning.
* Elements are dropped until `predicate` returns falsey. The predicate is
* invoked with three arguments: (value, index, array).
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The array to query.
* @param {Array|Function|Object|string} [predicate=_.identity]
* The function invoked per iteration.
* @returns {Array} Returns the slice of `array`.
* @example
*
* var users = [
* { 'user': 'barney', 'active': false },
* { 'user': 'fred', 'active': false },
* { 'user': 'pebbles', 'active': true }
* ];
*
* _.dropWhile(users, function(o) { return !o.active; });
* // => objects for ['pebbles']
*
* // The `_.matches` iteratee shorthand.
* _.dropWhile(users, { 'user': 'barney', 'active': false });
* // => objects for ['fred', 'pebbles']
*
* // The `_.matchesProperty` iteratee shorthand.
* _.dropWhile(users, ['active', false]);
* // => objects for ['pebbles']
*
* // The `_.property` iteratee shorthand.
* _.dropWhile(users, 'active');
* // => objects for ['barney', 'fred', 'pebbles']
*/
function dropWhile(array, predicate) {
return (array && array.length)
? _baseWhile(array, _baseIteratee(predicate, 3), true)
: [];
}
/* harmony default export */ var lodash_dropWhile = (dropWhile);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseClamp.js
/**
* The base implementation of `_.clamp` which doesn't coerce arguments to numbers.
*
* @private
* @param {number} number The number to clamp.
* @param {number} [lower] The lower bound.
* @param {number} upper The upper bound.
* @returns {number} Returns the clamped number.
*/
function baseClamp(number, lower, upper) {
if (number === number) {
if (upper !== undefined) {
number = number <= upper ? number : upper;
}
if (lower !== undefined) {
number = number >= lower ? number : lower;
}
}
return number;
}
/* harmony default export */ var _baseClamp = (baseClamp);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/toLength.js
/** Used as references for the maximum length and index of an array. */
var MAX_ARRAY_LENGTH = 4294967295;
/**
* Converts `value` to an integer suitable for use as the length of an
* array-like object.
*
* **Note:** This method is based on
* [`ToLength`](http://ecma-international.org/ecma-262/6.0/#sec-tolength).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to convert.
* @returns {number} Returns the converted integer.
* @example
*
* _.toLength(3.2);
* // => 3
*
* _.toLength(Number.MIN_VALUE);
* // => 0
*
* _.toLength(Infinity);
* // => 4294967295
*
* _.toLength('3.2');
* // => 3
*/
function toLength(value) {
return value ? _baseClamp(lodash_toInteger(value), 0, MAX_ARRAY_LENGTH) : 0;
}
/* harmony default export */ var lodash_toLength = (toLength);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseFill.js
/**
* The base implementation of `_.fill` without an iteratee call guard.
*
* @private
* @param {Array} array The array to fill.
* @param {*} value The value to fill `array` with.
* @param {number} [start=0] The start position.
* @param {number} [end=array.length] The end position.
* @returns {Array} Returns `array`.
*/
function baseFill(array, value, start, end) {
var length = array.length;
start = lodash_toInteger(start);
if (start < 0) {
start = -start > length ? 0 : (length + start);
}
end = (end === undefined || end > length) ? length : lodash_toInteger(end);
if (end < 0) {
end += length;
}
end = start > end ? 0 : lodash_toLength(end);
while (start < end) {
array[start++] = value;
}
return array;
}
/* harmony default export */ var _baseFill = (baseFill);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/fill.js
/**
* Fills elements of `array` with `value` from `start` up to, but not
* including, `end`.
*
* **Note:** This method mutates `array`.
*
* @static
* @memberOf _
* @since 3.2.0
* @category Array
* @param {Array} array The array to fill.
* @param {*} value The value to fill `array` with.
* @param {number} [start=0] The start position.
* @param {number} [end=array.length] The end position.
* @returns {Array} Returns `array`.
* @example
*
* var array = [1, 2, 3];
*
* _.fill(array, 'a');
* console.log(array);
* // => ['a', 'a', 'a']
*
* _.fill(Array(3), 2);
* // => [2, 2, 2]
*
* _.fill([4, 6, 8, 10], '*', 1, 3);
* // => [4, '*', '*', 10]
*/
function fill(array, value, start, end) {
var length = array ? array.length : 0;
if (!length) {
return [];
}
if (start && typeof start != 'number' && _isIterateeCall(array, value, start)) {
start = 0;
end = length;
}
return _baseFill(array, value, start, end);
}
/* harmony default export */ var lodash_fill = (fill);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseFindIndex.js
/**
* The base implementation of `_.findIndex` and `_.findLastIndex` without
* support for iteratee shorthands.
*
* @private
* @param {Array} array The array to search.
* @param {Function} predicate The function invoked per iteration.
* @param {boolean} [fromRight] Specify iterating from right to left.
* @returns {number} Returns the index of the matched value, else `-1`.
*/
function baseFindIndex(array, predicate, fromRight) {
var length = array.length,
index = fromRight ? length : -1;
while ((fromRight ? index-- : ++index < length)) {
if (predicate(array[index], index, array)) {
return index;
}
}
return -1;
}
/* harmony default export */ var _baseFindIndex = (baseFindIndex);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/findIndex.js
/**
* This method is like `_.find` except that it returns the index of the first
* element `predicate` returns truthy for instead of the element itself.
*
* @static
* @memberOf _
* @since 1.1.0
* @category Array
* @param {Array} array The array to search.
* @param {Array|Function|Object|string} [predicate=_.identity]
* The function invoked per iteration.
* @returns {number} Returns the index of the found element, else `-1`.
* @example
*
* var users = [
* { 'user': 'barney', 'active': false },
* { 'user': 'fred', 'active': false },
* { 'user': 'pebbles', 'active': true }
* ];
*
* _.findIndex(users, function(o) { return o.user == 'barney'; });
* // => 0
*
* // The `_.matches` iteratee shorthand.
* _.findIndex(users, { 'user': 'fred', 'active': false });
* // => 1
*
* // The `_.matchesProperty` iteratee shorthand.
* _.findIndex(users, ['active', false]);
* // => 0
*
* // The `_.property` iteratee shorthand.
* _.findIndex(users, 'active');
* // => 2
*/
function findIndex(array, predicate) {
return (array && array.length)
? _baseFindIndex(array, _baseIteratee(predicate, 3))
: -1;
}
/* harmony default export */ var lodash_findIndex = (findIndex);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/findLastIndex.js
/**
* This method is like `_.findIndex` except that it iterates over elements
* of `collection` from right to left.
*
* @static
* @memberOf _
* @since 2.0.0
* @category Array
* @param {Array} array The array to search.
* @param {Array|Function|Object|string} [predicate=_.identity]
* The function invoked per iteration.
* @returns {number} Returns the index of the found element, else `-1`.
* @example
*
* var users = [
* { 'user': 'barney', 'active': true },
* { 'user': 'fred', 'active': false },
* { 'user': 'pebbles', 'active': false }
* ];
*
* _.findLastIndex(users, function(o) { return o.user == 'pebbles'; });
* // => 2
*
* // The `_.matches` iteratee shorthand.
* _.findLastIndex(users, { 'user': 'barney', 'active': true });
* // => 0
*
* // The `_.matchesProperty` iteratee shorthand.
* _.findLastIndex(users, ['active', false]);
* // => 2
*
* // The `_.property` iteratee shorthand.
* _.findLastIndex(users, 'active');
* // => 0
*/
function findLastIndex(array, predicate) {
return (array && array.length)
? _baseFindIndex(array, _baseIteratee(predicate, 3), true)
: -1;
}
/* harmony default export */ var lodash_findLastIndex = (findLastIndex);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/head.js
/**
* Gets the first element of `array`.
*
* @static
* @memberOf _
* @since 0.1.0
* @alias first
* @category Array
* @param {Array} array The array to query.
* @returns {*} Returns the first element of `array`.
* @example
*
* _.head([1, 2, 3]);
* // => 1
*
* _.head([]);
* // => undefined
*/
function head(array) {
return (array && array.length) ? array[0] : undefined;
}
/* harmony default export */ var lodash_head = (head);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/first.js
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/flatten.js
/**
* Flattens `array` a single level deep.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to flatten.
* @returns {Array} Returns the new flattened array.
* @example
*
* _.flatten([1, [2, [3, [4]], 5]]);
* // => [1, 2, [3, [4]], 5]
*/
function flatten(array) {
var length = array ? array.length : 0;
return length ? _baseFlatten(array, 1) : [];
}
/* harmony default export */ var lodash_flatten = (flatten);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/flattenDeep.js
/** Used as references for various `Number` constants. */
var flattenDeep_INFINITY = 1 / 0;
/**
* Recursively flattens `array`.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The array to flatten.
* @returns {Array} Returns the new flattened array.
* @example
*
* _.flattenDeep([1, [2, [3, [4]], 5]]);
* // => [1, 2, 3, 4, 5]
*/
function flattenDeep(array) {
var length = array ? array.length : 0;
return length ? _baseFlatten(array, flattenDeep_INFINITY) : [];
}
/* harmony default export */ var lodash_flattenDeep = (flattenDeep);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/flattenDepth.js
/**
* Recursively flatten `array` up to `depth` times.
*
* @static
* @memberOf _
* @since 4.4.0
* @category Array
* @param {Array} array The array to flatten.
* @param {number} [depth=1] The maximum recursion depth.
* @returns {Array} Returns the new flattened array.
* @example
*
* var array = [1, [2, [3, [4]], 5]];
*
* _.flattenDepth(array, 1);
* // => [1, 2, [3, [4]], 5]
*
* _.flattenDepth(array, 2);
* // => [1, 2, 3, [4], 5]
*/
function flattenDepth(array, depth) {
var length = array ? array.length : 0;
if (!length) {
return [];
}
depth = depth === undefined ? 1 : lodash_toInteger(depth);
return _baseFlatten(array, depth);
}
/* harmony default export */ var lodash_flattenDepth = (flattenDepth);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/fromPairs.js
/**
* The inverse of `_.toPairs`; this method returns an object composed
* from key-value `pairs`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} pairs The key-value pairs.
* @returns {Object} Returns the new object.
* @example
*
* _.fromPairs([['fred', 30], ['barney', 40]]);
* // => { 'fred': 30, 'barney': 40 }
*/
function fromPairs(pairs) {
var index = -1,
length = pairs ? pairs.length : 0,
result = {};
while (++index < length) {
var pair = pairs[index];
result[pair[0]] = pair[1];
}
return result;
}
/* harmony default export */ var lodash_fromPairs = (fromPairs);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/indexOf.js
/* Built-in method references for those with the same name as other `lodash` methods. */
var indexOf_nativeMax = Math.max;
/**
* Gets the index at which the first occurrence of `value` is found in `array`
* using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
* for equality comparisons. If `fromIndex` is negative, it's used as the
* offset from the end of `array`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to search.
* @param {*} value The value to search for.
* @param {number} [fromIndex=0] The index to search from.
* @returns {number} Returns the index of the matched value, else `-1`.
* @example
*
* _.indexOf([1, 2, 1, 2], 2);
* // => 1
*
* // Search from the `fromIndex`.
* _.indexOf([1, 2, 1, 2], 2, 2);
* // => 3
*/
function indexOf_indexOf(array, value, fromIndex) {
var length = array ? array.length : 0;
if (!length) {
return -1;
}
fromIndex = lodash_toInteger(fromIndex);
if (fromIndex < 0) {
fromIndex = indexOf_nativeMax(length + fromIndex, 0);
}
return _baseIndexOf(array, value, fromIndex);
}
/* harmony default export */ var lodash_indexOf = (indexOf_indexOf);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/initial.js
/**
* Gets all but the last element of `array`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to query.
* @returns {Array} Returns the slice of `array`.
* @example
*
* _.initial([1, 2, 3]);
* // => [1, 2]
*/
function initial(array) {
return lodash_dropRight(array, 1);
}
/* harmony default export */ var lodash_initial = (initial);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseIntersection.js
/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeMin = Math.min;
/**
* The base implementation of methods like `_.intersection`, without support
* for iteratee shorthands, that accepts an array of arrays to inspect.
*
* @private
* @param {Array} arrays The arrays to inspect.
* @param {Function} [iteratee] The iteratee invoked per element.
* @param {Function} [comparator] The comparator invoked per element.
* @returns {Array} Returns the new array of shared values.
*/
function baseIntersection(arrays, iteratee, comparator) {
var includes = comparator ? _arrayIncludesWith : _arrayIncludes,
length = arrays[0].length,
othLength = arrays.length,
othIndex = othLength,
caches = Array(othLength),
maxLength = Infinity,
result = [];
while (othIndex--) {
var array = arrays[othIndex];
if (othIndex && iteratee) {
array = _arrayMap(array, _baseUnary(iteratee));
}
maxLength = nativeMin(array.length, maxLength);
caches[othIndex] = !comparator && (iteratee || (length >= 120 && array.length >= 120))
? new _SetCache(othIndex && array)
: undefined;
}
array = arrays[0];
var index = -1,
seen = caches[0];
outer:
while (++index < length && result.length < maxLength) {
var value = array[index],
computed = iteratee ? iteratee(value) : value;
value = (comparator || value !== 0) ? value : 0;
if (!(seen
? _cacheHas(seen, computed)
: includes(result, computed, comparator)
)) {
othIndex = othLength;
while (--othIndex) {
var cache = caches[othIndex];
if (!(cache
? _cacheHas(cache, computed)
: includes(arrays[othIndex], computed, comparator))
) {
continue outer;
}
}
if (seen) {
seen.push(computed);
}
result.push(value);
}
}
return result;
}
/* harmony default export */ var _baseIntersection = (baseIntersection);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_castArrayLikeObject.js
/**
* Casts `value` to an empty array if it's not an array like object.
*
* @private
* @param {*} value The value to inspect.
* @returns {Array|Object} Returns the cast array-like object.
*/
function castArrayLikeObject(value) {
return lodash_isArrayLikeObject(value) ? value : [];
}
/* harmony default export */ var _castArrayLikeObject = (castArrayLikeObject);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/intersection.js
/**
* Creates an array of unique values that are included in all given arrays
* using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
* for equality comparisons. The order of result values is determined by the
* order they occur in the first array.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {...Array} [arrays] The arrays to inspect.
* @returns {Array} Returns the new array of intersecting values.
* @example
*
* _.intersection([2, 1], [4, 2], [1, 2]);
* // => [2]
*/
var intersection = lodash_rest(function(arrays) {
var mapped = _arrayMap(arrays, _castArrayLikeObject);
return (mapped.length && mapped[0] === arrays[0])
? _baseIntersection(mapped)
: [];
});
/* harmony default export */ var lodash_intersection = (intersection);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/intersectionBy.js
/**
* This method is like `_.intersection` except that it accepts `iteratee`
* which is invoked for each element of each `arrays` to generate the criterion
* by which they're compared. Result values are chosen from the first array.
* The iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {...Array} [arrays] The arrays to inspect.
* @param {Array|Function|Object|string} [iteratee=_.identity]
* The iteratee invoked per element.
* @returns {Array} Returns the new array of intersecting values.
* @example
*
* _.intersectionBy([2.1, 1.2], [4.3, 2.4], Math.floor);
* // => [2.1]
*
* // The `_.property` iteratee shorthand.
* _.intersectionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');
* // => [{ 'x': 1 }]
*/
var intersectionBy = lodash_rest(function(arrays) {
var iteratee = lodash_last(arrays),
mapped = _arrayMap(arrays, _castArrayLikeObject);
if (iteratee === lodash_last(mapped)) {
iteratee = undefined;
} else {
mapped.pop();
}
return (mapped.length && mapped[0] === arrays[0])
? _baseIntersection(mapped, _baseIteratee(iteratee))
: [];
});
/* harmony default export */ var lodash_intersectionBy = (intersectionBy);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/intersectionWith.js
/**
* This method is like `_.intersection` except that it accepts `comparator`
* which is invoked to compare elements of `arrays`. Result values are chosen
* from the first array. The comparator is invoked with two arguments:
* (arrVal, othVal).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {...Array} [arrays] The arrays to inspect.
* @param {Function} [comparator] The comparator invoked per element.
* @returns {Array} Returns the new array of intersecting values.
* @example
*
* var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];
* var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];
*
* _.intersectionWith(objects, others, _.isEqual);
* // => [{ 'x': 1, 'y': 2 }]
*/
var intersectionWith = lodash_rest(function(arrays) {
var comparator = lodash_last(arrays),
mapped = _arrayMap(arrays, _castArrayLikeObject);
if (comparator === lodash_last(mapped)) {
comparator = undefined;
} else {
mapped.pop();
}
return (mapped.length && mapped[0] === arrays[0])
? _baseIntersection(mapped, undefined, comparator)
: [];
});
/* harmony default export */ var lodash_intersectionWith = (intersectionWith);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/join.js
/** Used for built-in method references. */
var join_arrayProto = Array.prototype;
/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeJoin = join_arrayProto.join;
/**
* Converts all elements in `array` into a string separated by `separator`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to convert.
* @param {string} [separator=','] The element separator.
* @returns {string} Returns the joined string.
* @example
*
* _.join(['a', 'b', 'c'], '~');
* // => 'a~b~c'
*/
function join(array, separator) {
return array ? nativeJoin.call(array, separator) : '';
}
/* harmony default export */ var lodash_join = (join);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/lastIndexOf.js
/* Built-in method references for those with the same name as other `lodash` methods. */
var lastIndexOf_nativeMax = Math.max,
lastIndexOf_nativeMin = Math.min;
/**
* This method is like `_.indexOf` except that it iterates over elements of
* `array` from right to left.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to search.
* @param {*} value The value to search for.
* @param {number} [fromIndex=array.length-1] The index to search from.
* @returns {number} Returns the index of the matched value, else `-1`.
* @example
*
* _.lastIndexOf([1, 2, 1, 2], 2);
* // => 3
*
* // Search from the `fromIndex`.
* _.lastIndexOf([1, 2, 1, 2], 2, 2);
* // => 1
*/
function lastIndexOf(array, value, fromIndex) {
var length = array ? array.length : 0;
if (!length) {
return -1;
}
var index = length;
if (fromIndex !== undefined) {
index = lodash_toInteger(fromIndex);
index = (
index < 0
? lastIndexOf_nativeMax(length + index, 0)
: lastIndexOf_nativeMin(index, length - 1)
) + 1;
}
if (value !== value) {
return _indexOfNaN(array, index, true);
}
while (index--) {
if (array[index] === value) {
return index;
}
}
return -1;
}
/* harmony default export */ var lodash_lastIndexOf = (lastIndexOf);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseNth.js
/**
* The base implementation of `_.nth` which doesn't coerce `n` to an integer.
*
* @private
* @param {Array} array The array to query.
* @param {number} n The index of the element to return.
* @returns {*} Returns the nth element of `array`.
*/
function baseNth(array, n) {
var length = array.length;
if (!length) {
return;
}
n += n < 0 ? length : 0;
return _isIndex(n, length) ? array[n] : undefined;
}
/* harmony default export */ var _baseNth = (baseNth);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/nth.js
/**
* Gets the element at `n` index of `array`. If `n` is negative, the nth
* element from the end is returned.
*
* @static
* @memberOf _
* @since 4.11.0
* @category Array
* @param {Array} array The array to query.
* @param {number} [n=0] The index of the element to return.
* @returns {*} Returns the nth element of `array`.
* @example
*
* var array = ['a', 'b', 'c', 'd'];
*
* _.nth(array, 1);
* // => 'b'
*
* _.nth(array, -2);
* // => 'c';
*/
function nth(array, n) {
return (array && array.length) ? _baseNth(array, lodash_toInteger(n)) : undefined;
}
/* harmony default export */ var lodash_nth = (nth);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseIndexOfWith.js
/**
* This function is like `baseIndexOf` except that it accepts a comparator.
*
* @private
* @param {Array} array The array to search.
* @param {*} value The value to search for.
* @param {number} fromIndex The index to search from.
* @param {Function} comparator The comparator invoked per element.
* @returns {number} Returns the index of the matched value, else `-1`.
*/
function baseIndexOfWith(array, value, fromIndex, comparator) {
var index = fromIndex - 1,
length = array.length;
while (++index < length) {
if (comparator(array[index], value)) {
return index;
}
}
return -1;
}
/* harmony default export */ var _baseIndexOfWith = (baseIndexOfWith);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_basePullAll.js
/** Used for built-in method references. */
var _basePullAll_arrayProto = Array.prototype;
/** Built-in value references. */
var _basePullAll_splice = _basePullAll_arrayProto.splice;
/**
* The base implementation of `_.pullAllBy` without support for iteratee
* shorthands.
*
* @private
* @param {Array} array The array to modify.
* @param {Array} values The values to remove.
* @param {Function} [iteratee] The iteratee invoked per element.
* @param {Function} [comparator] The comparator invoked per element.
* @returns {Array} Returns `array`.
*/
function basePullAll(array, values, iteratee, comparator) {
var indexOf = comparator ? _baseIndexOfWith : _baseIndexOf,
index = -1,
length = values.length,
seen = array;
if (iteratee) {
seen = _arrayMap(array, _baseUnary(iteratee));
}
while (++index < length) {
var fromIndex = 0,
value = values[index],
computed = iteratee ? iteratee(value) : value;
while ((fromIndex = indexOf(seen, computed, fromIndex, comparator)) > -1) {
if (seen !== array) {
_basePullAll_splice.call(seen, fromIndex, 1);
}
_basePullAll_splice.call(array, fromIndex, 1);
}
}
return array;
}
/* harmony default export */ var _basePullAll = (basePullAll);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/pullAll.js
/**
* This method is like `_.pull` except that it accepts an array of values to remove.
*
* **Note:** Unlike `_.difference`, this method mutates `array`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to modify.
* @param {Array} values The values to remove.
* @returns {Array} Returns `array`.
* @example
*
* var array = [1, 2, 3, 1, 2, 3];
*
* _.pullAll(array, [2, 3]);
* console.log(array);
* // => [1, 1]
*/
function pullAll(array, values) {
return (array && array.length && values && values.length)
? _basePullAll(array, values)
: array;
}
/* harmony default export */ var lodash_pullAll = (pullAll);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/pull.js
/**
* Removes all given values from `array` using
* [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
* for equality comparisons.
*
* **Note:** Unlike `_.without`, this method mutates `array`. Use `_.remove`
* to remove elements from an array by predicate.
*
* @static
* @memberOf _
* @since 2.0.0
* @category Array
* @param {Array} array The array to modify.
* @param {...*} [values] The values to remove.
* @returns {Array} Returns `array`.
* @example
*
* var array = [1, 2, 3, 1, 2, 3];
*
* _.pull(array, 2, 3);
* console.log(array);
* // => [1, 1]
*/
var pull = lodash_rest(lodash_pullAll);
/* harmony default export */ var lodash_pull = (pull);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/pullAllBy.js
/**
* This method is like `_.pullAll` except that it accepts `iteratee` which is
* invoked for each element of `array` and `values` to generate the criterion
* by which they're compared. The iteratee is invoked with one argument: (value).
*
* **Note:** Unlike `_.differenceBy`, this method mutates `array`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to modify.
* @param {Array} values The values to remove.
* @param {Array|Function|Object|string} [iteratee=_.identity]
* The iteratee invoked per element.
* @returns {Array} Returns `array`.
* @example
*
* var array = [{ 'x': 1 }, { 'x': 2 }, { 'x': 3 }, { 'x': 1 }];
*
* _.pullAllBy(array, [{ 'x': 1 }, { 'x': 3 }], 'x');
* console.log(array);
* // => [{ 'x': 2 }]
*/
function pullAllBy(array, values, iteratee) {
return (array && array.length && values && values.length)
? _basePullAll(array, values, _baseIteratee(iteratee))
: array;
}
/* harmony default export */ var lodash_pullAllBy = (pullAllBy);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/pullAllWith.js
/**
* This method is like `_.pullAll` except that it accepts `comparator` which
* is invoked to compare elements of `array` to `values`. The comparator is
* invoked with two arguments: (arrVal, othVal).
*
* **Note:** Unlike `_.differenceWith`, this method mutates `array`.
*
* @static
* @memberOf _
* @since 4.6.0
* @category Array
* @param {Array} array The array to modify.
* @param {Array} values The values to remove.
* @param {Function} [comparator] The comparator invoked per element.
* @returns {Array} Returns `array`.
* @example
*
* var array = [{ 'x': 1, 'y': 2 }, { 'x': 3, 'y': 4 }, { 'x': 5, 'y': 6 }];
*
* _.pullAllWith(array, [{ 'x': 3, 'y': 4 }], _.isEqual);
* console.log(array);
* // => [{ 'x': 1, 'y': 2 }, { 'x': 5, 'y': 6 }]
*/
function pullAllWith(array, values, comparator) {
return (array && array.length && values && values.length)
? _basePullAll(array, values, undefined, comparator)
: array;
}
/* harmony default export */ var lodash_pullAllWith = (pullAllWith);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseAt.js
/**
* The base implementation of `_.at` without support for individual paths.
*
* @private
* @param {Object} object The object to iterate over.
* @param {string[]} paths The property paths of elements to pick.
* @returns {Array} Returns the picked elements.
*/
function baseAt(object, paths) {
var index = -1,
isNil = object == null,
length = paths.length,
result = Array(length);
while (++index < length) {
result[index] = isNil ? undefined : lodash_get(object, paths[index]);
}
return result;
}
/* harmony default export */ var _baseAt = (baseAt);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_parent.js
/**
* Gets the parent value at `path` of `object`.
*
* @private
* @param {Object} object The object to query.
* @param {Array} path The path to get the parent value of.
* @returns {*} Returns the parent value.
*/
function _parent_parent(object, path) {
return path.length == 1 ? object : _baseGet(object, _baseSlice(path, 0, -1));
}
/* harmony default export */ var _parent = (_parent_parent);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_basePullAt.js
/** Used for built-in method references. */
var _basePullAt_arrayProto = Array.prototype;
/** Built-in value references. */
var _basePullAt_splice = _basePullAt_arrayProto.splice;
/**
* The base implementation of `_.pullAt` without support for individual
* indexes or capturing the removed elements.
*
* @private
* @param {Array} array The array to modify.
* @param {number[]} indexes The indexes of elements to remove.
* @returns {Array} Returns `array`.
*/
function basePullAt(array, indexes) {
var length = array ? indexes.length : 0,
lastIndex = length - 1;
while (length--) {
var index = indexes[length];
if (length == lastIndex || index !== previous) {
var previous = index;
if (_isIndex(index)) {
_basePullAt_splice.call(array, index, 1);
}
else if (!_isKey(index, array)) {
var path = _castPath(index),
object = _parent(array, path);
if (object != null) {
delete object[_toKey(lodash_last(path))];
}
}
else {
delete array[_toKey(index)];
}
}
}
return array;
}
/* harmony default export */ var _basePullAt = (basePullAt);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_compareAscending.js
/**
* Compares values to sort them in ascending order.
*
* @private
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @returns {number} Returns the sort order indicator for `value`.
*/
function compareAscending(value, other) {
if (value !== other) {
var valIsDefined = value !== undefined,
valIsNull = value === null,
valIsReflexive = value === value,
valIsSymbol = lodash_isSymbol(value);
var othIsDefined = other !== undefined,
othIsNull = other === null,
othIsReflexive = other === other,
othIsSymbol = lodash_isSymbol(other);
if ((!othIsNull && !othIsSymbol && !valIsSymbol && value > other) ||
(valIsSymbol && othIsDefined && othIsReflexive && !othIsNull && !othIsSymbol) ||
(valIsNull && othIsDefined && othIsReflexive) ||
(!valIsDefined && othIsReflexive) ||
!valIsReflexive) {
return 1;
}
if ((!valIsNull && !valIsSymbol && !othIsSymbol && value < other) ||
(othIsSymbol && valIsDefined && valIsReflexive && !valIsNull && !valIsSymbol) ||
(othIsNull && valIsDefined && valIsReflexive) ||
(!othIsDefined && valIsReflexive) ||
!othIsReflexive) {
return -1;
}
}
return 0;
}
/* harmony default export */ var _compareAscending = (compareAscending);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/pullAt.js
/**
* Removes elements from `array` corresponding to `indexes` and returns an
* array of removed elements.
*
* **Note:** Unlike `_.at`, this method mutates `array`.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The array to modify.
* @param {...(number|number[])} [indexes] The indexes of elements to remove.
* @returns {Array} Returns the new array of removed elements.
* @example
*
* var array = [5, 10, 15, 20];
* var evens = _.pullAt(array, 1, 3);
*
* console.log(array);
* // => [5, 15]
*
* console.log(evens);
* // => [10, 20]
*/
var pullAt = lodash_rest(function(array, indexes) {
indexes = _baseFlatten(indexes, 1);
var length = array ? array.length : 0,
result = _baseAt(array, indexes);
_basePullAt(array, _arrayMap(indexes, function(index) {
return _isIndex(index, length) ? +index : index;
}).sort(_compareAscending));
return result;
});
/* harmony default export */ var lodash_pullAt = (pullAt);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/remove.js
/**
* Removes all elements from `array` that `predicate` returns truthy for
* and returns an array of the removed elements. The predicate is invoked
* with three arguments: (value, index, array).
*
* **Note:** Unlike `_.filter`, this method mutates `array`. Use `_.pull`
* to pull elements from an array by value.
*
* @static
* @memberOf _
* @since 2.0.0
* @category Array
* @param {Array} array The array to modify.
* @param {Array|Function|Object|string} [predicate=_.identity]
* The function invoked per iteration.
* @returns {Array} Returns the new array of removed elements.
* @example
*
* var array = [1, 2, 3, 4];
* var evens = _.remove(array, function(n) {
* return n % 2 == 0;
* });
*
* console.log(array);
* // => [1, 3]
*
* console.log(evens);
* // => [2, 4]
*/
function remove_remove(array, predicate) {
var result = [];
if (!(array && array.length)) {
return result;
}
var index = -1,
indexes = [],
length = array.length;
predicate = _baseIteratee(predicate, 3);
while (++index < length) {
var value = array[index];
if (predicate(value, index, array)) {
result.push(value);
indexes.push(index);
}
}
_basePullAt(array, indexes);
return result;
}
/* harmony default export */ var lodash_remove = (remove_remove);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/reverse.js
/** Used for built-in method references. */
var reverse_arrayProto = Array.prototype;
/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeReverse = reverse_arrayProto.reverse;
/**
* Reverses `array` so that the first element becomes the last, the second
* element becomes the second to last, and so on.
*
* **Note:** This method mutates `array` and is based on
* [`Array#reverse`](https://mdn.io/Array/reverse).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to modify.
* @returns {Array} Returns `array`.
* @example
*
* var array = [1, 2, 3];
*
* _.reverse(array);
* // => [3, 2, 1]
*
* console.log(array);
* // => [3, 2, 1]
*/
function reverse(array) {
return array ? nativeReverse.call(array) : array;
}
/* harmony default export */ var lodash_reverse = (reverse);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/slice.js
/**
* Creates a slice of `array` from `start` up to, but not including, `end`.
*
* **Note:** This method is used instead of
* [`Array#slice`](https://mdn.io/Array/slice) to ensure dense arrays are
* returned.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The array to slice.
* @param {number} [start=0] The start position.
* @param {number} [end=array.length] The end position.
* @returns {Array} Returns the slice of `array`.
*/
function slice(array, start, end) {
var length = array ? array.length : 0;
if (!length) {
return [];
}
if (end && typeof end != 'number' && _isIterateeCall(array, start, end)) {
start = 0;
end = length;
}
else {
start = start == null ? 0 : lodash_toInteger(start);
end = end === undefined ? length : lodash_toInteger(end);
}
return _baseSlice(array, start, end);
}
/* harmony default export */ var lodash_slice = (slice);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseSortedIndexBy.js
/** Used as references for the maximum length and index of an array. */
var _baseSortedIndexBy_MAX_ARRAY_LENGTH = 4294967295,
MAX_ARRAY_INDEX = _baseSortedIndexBy_MAX_ARRAY_LENGTH - 1;
/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeFloor = Math.floor,
_baseSortedIndexBy_nativeMin = Math.min;
/**
* The base implementation of `_.sortedIndexBy` and `_.sortedLastIndexBy`
* which invokes `iteratee` for `value` and each element of `array` to compute
* their sort ranking. The iteratee is invoked with one argument; (value).
*
* @private
* @param {Array} array The sorted array to inspect.
* @param {*} value The value to evaluate.
* @param {Function} iteratee The iteratee invoked per element.
* @param {boolean} [retHighest] Specify returning the highest qualified index.
* @returns {number} Returns the index at which `value` should be inserted
* into `array`.
*/
function baseSortedIndexBy(array, value, iteratee, retHighest) {
value = iteratee(value);
var low = 0,
high = array ? array.length : 0,
valIsNaN = value !== value,
valIsNull = value === null,
valIsSymbol = lodash_isSymbol(value),
valIsUndefined = value === undefined;
while (low < high) {
var mid = nativeFloor((low + high) / 2),
computed = iteratee(array[mid]),
othIsDefined = computed !== undefined,
othIsNull = computed === null,
othIsReflexive = computed === computed,
othIsSymbol = lodash_isSymbol(computed);
if (valIsNaN) {
var setLow = retHighest || othIsReflexive;
} else if (valIsUndefined) {
setLow = othIsReflexive && (retHighest || othIsDefined);
} else if (valIsNull) {
setLow = othIsReflexive && othIsDefined && (retHighest || !othIsNull);
} else if (valIsSymbol) {
setLow = othIsReflexive && othIsDefined && !othIsNull && (retHighest || !othIsSymbol);
} else if (othIsNull || othIsSymbol) {
setLow = false;
} else {
setLow = retHighest ? (computed <= value) : (computed < value);
}
if (setLow) {
low = mid + 1;
} else {
high = mid;
}
}
return _baseSortedIndexBy_nativeMin(high, MAX_ARRAY_INDEX);
}
/* harmony default export */ var _baseSortedIndexBy = (baseSortedIndexBy);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseSortedIndex.js
/** Used as references for the maximum length and index of an array. */
var _baseSortedIndex_MAX_ARRAY_LENGTH = 4294967295,
HALF_MAX_ARRAY_LENGTH = _baseSortedIndex_MAX_ARRAY_LENGTH >>> 1;
/**
* The base implementation of `_.sortedIndex` and `_.sortedLastIndex` which
* performs a binary search of `array` to determine the index at which `value`
* should be inserted into `array` in order to maintain its sort order.
*
* @private
* @param {Array} array The sorted array to inspect.
* @param {*} value The value to evaluate.
* @param {boolean} [retHighest] Specify returning the highest qualified index.
* @returns {number} Returns the index at which `value` should be inserted
* into `array`.
*/
function baseSortedIndex(array, value, retHighest) {
var low = 0,
high = array ? array.length : low;
if (typeof value == 'number' && value === value && high <= HALF_MAX_ARRAY_LENGTH) {
while (low < high) {
var mid = (low + high) >>> 1,
computed = array[mid];
if (computed !== null && !lodash_isSymbol(computed) &&
(retHighest ? (computed <= value) : (computed < value))) {
low = mid + 1;
} else {
high = mid;
}
}
return high;
}
return _baseSortedIndexBy(array, value, lodash_identity, retHighest);
}
/* harmony default export */ var _baseSortedIndex = (baseSortedIndex);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/sortedIndex.js
/**
* Uses a binary search to determine the lowest index at which `value`
* should be inserted into `array` in order to maintain its sort order.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The sorted array to inspect.
* @param {*} value The value to evaluate.
* @returns {number} Returns the index at which `value` should be inserted
* into `array`.
* @example
*
* _.sortedIndex([30, 50], 40);
* // => 1
*
* _.sortedIndex([4, 5], 4);
* // => 0
*/
function sortedIndex(array, value) {
return _baseSortedIndex(array, value);
}
/* harmony default export */ var lodash_sortedIndex = (sortedIndex);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/sortedIndexBy.js
/**
* This method is like `_.sortedIndex` except that it accepts `iteratee`
* which is invoked for `value` and each element of `array` to compute their
* sort ranking. The iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The sorted array to inspect.
* @param {*} value The value to evaluate.
* @param {Array|Function|Object|string} [iteratee=_.identity]
* The iteratee invoked per element.
* @returns {number} Returns the index at which `value` should be inserted
* into `array`.
* @example
*
* var dict = { 'thirty': 30, 'forty': 40, 'fifty': 50 };
*
* _.sortedIndexBy(['thirty', 'fifty'], 'forty', _.propertyOf(dict));
* // => 1
*
* // The `_.property` iteratee shorthand.
* _.sortedIndexBy([{ 'x': 4 }, { 'x': 5 }], { 'x': 4 }, 'x');
* // => 0
*/
function sortedIndexBy(array, value, iteratee) {
return _baseSortedIndexBy(array, value, _baseIteratee(iteratee));
}
/* harmony default export */ var lodash_sortedIndexBy = (sortedIndexBy);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/sortedIndexOf.js
/**
* This method is like `_.indexOf` except that it performs a binary
* search on a sorted `array`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to search.
* @param {*} value The value to search for.
* @returns {number} Returns the index of the matched value, else `-1`.
* @example
*
* _.sortedIndexOf([1, 1, 2, 2], 2);
* // => 2
*/
function sortedIndexOf(array, value) {
var length = array ? array.length : 0;
if (length) {
var index = _baseSortedIndex(array, value);
if (index < length && lodash_eq(array[index], value)) {
return index;
}
}
return -1;
}
/* harmony default export */ var lodash_sortedIndexOf = (sortedIndexOf);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/sortedLastIndex.js
/**
* This method is like `_.sortedIndex` except that it returns the highest
* index at which `value` should be inserted into `array` in order to
* maintain its sort order.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Array
* @param {Array} array The sorted array to inspect.
* @param {*} value The value to evaluate.
* @returns {number} Returns the index at which `value` should be inserted
* into `array`.
* @example
*
* _.sortedLastIndex([4, 5], 4);
* // => 1
*/
function sortedLastIndex(array, value) {
return _baseSortedIndex(array, value, true);
}
/* harmony default export */ var lodash_sortedLastIndex = (sortedLastIndex);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/sortedLastIndexBy.js
/**
* This method is like `_.sortedLastIndex` except that it accepts `iteratee`
* which is invoked for `value` and each element of `array` to compute their
* sort ranking. The iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The sorted array to inspect.
* @param {*} value The value to evaluate.
* @param {Array|Function|Object|string} [iteratee=_.identity]
* The iteratee invoked per element.
* @returns {number} Returns the index at which `value` should be inserted
* into `array`.
* @example
*
* // The `_.property` iteratee shorthand.
* _.sortedLastIndexBy([{ 'x': 4 }, { 'x': 5 }], { 'x': 4 }, 'x');
* // => 1
*/
function sortedLastIndexBy(array, value, iteratee) {
return _baseSortedIndexBy(array, value, _baseIteratee(iteratee), true);
}
/* harmony default export */ var lodash_sortedLastIndexBy = (sortedLastIndexBy);
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/sortedLastIndexOf.js
/**
* This method is like `_.lastIndexOf` except that it performs a binary
* search on a sorted `array`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to search.
* @param {*} value The value to search for.
* @returns {number} Returns the index of the matched value, else `-1`.
* @example
*
* _.sortedLastIndexOf([1, 1, 2, 2], 2);
* // => 3
*/
function sortedLastIndexOf(array, value) {
var length = array ? array.length : 0;
if (length) {
var index = _baseSortedIndex(array, value, true) - 1;
if (lodash_eq(array[index], value
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment